diff --git a/.gitattributes b/.gitattributes index a6344aac8c09253b3b630fb776ae94478aa0275b..0a6358f73685d7ccf6c5c4ba6b2062ee1ad422a6 100644 --- a/.gitattributes +++ b/.gitattributes @@ -33,3 +33,35 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text *.zip filter=lfs diff=lfs merge=lfs -text *.zst filter=lfs diff=lfs merge=lfs -text *tfevents* filter=lfs diff=lfs merge=lfs -text +assets/r3pmnet_overview.png filter=lfs diff=lfs merge=lfs -text +assets/sioux_cranfield.png filter=lfs diff=lfs merge=lfs -text +assets/sioux_scans.png filter=lfs diff=lfs merge=lfs -text +assets/success_cases.png filter=lfs diff=lfs merge=lfs -text +assets/teaser.png filter=lfs diff=lfs merge=lfs -text +thirdparty/learning3d/pretrained/exp_classifier/models/best_model_snap.t7 filter=lfs diff=lfs merge=lfs -text +thirdparty/learning3d/pretrained/exp_classifier/models/best_model.t7 filter=lfs diff=lfs merge=lfs -text +thirdparty/learning3d/pretrained/exp_classifier/models/best_ptnet_model.t7 filter=lfs diff=lfs merge=lfs -text +thirdparty/learning3d/pretrained/exp_curvenet/models/model.t7 filter=lfs diff=lfs merge=lfs -text +thirdparty/learning3d/pretrained/exp_dcp/models/best_model.t7 filter=lfs diff=lfs merge=lfs -text +thirdparty/learning3d/pretrained/exp_flownet/models/model.best.t7 filter=lfs diff=lfs merge=lfs -text +thirdparty/learning3d/pretrained/exp_ipcrnet/models/best_model_v1.t7 filter=lfs diff=lfs merge=lfs -text +thirdparty/learning3d/pretrained/exp_ipcrnet/models/best_model.t7 filter=lfs diff=lfs merge=lfs -text +thirdparty/learning3d/pretrained/exp_ipcrnet/models/best_ptnet_model.t7 filter=lfs diff=lfs merge=lfs -text +thirdparty/learning3d/pretrained/exp_masknet/models/best_model.t7 filter=lfs diff=lfs merge=lfs -text +thirdparty/learning3d/pretrained/exp_masknet2/models/best_model_0.01.t7 filter=lfs diff=lfs merge=lfs -text +thirdparty/learning3d/pretrained/exp_masknet2/models/best_model_0.6.t7 filter=lfs diff=lfs merge=lfs -text +thirdparty/learning3d/pretrained/exp_masknet2/models/best_model_0.7.t7 filter=lfs diff=lfs merge=lfs -text +thirdparty/learning3d/pretrained/exp_masknet2/models/best_model_0.8.t7 filter=lfs diff=lfs merge=lfs -text +thirdparty/learning3d/pretrained/exp_masknet2/models/best_model_0.9.t7 filter=lfs diff=lfs merge=lfs -text +thirdparty/learning3d/pretrained/exp_masknet2/models/best_model_100.t7 filter=lfs diff=lfs merge=lfs -text +thirdparty/learning3d/pretrained/exp_masknet2/models/best_model_200.t7 filter=lfs diff=lfs merge=lfs -text +thirdparty/learning3d/pretrained/exp_masknet2/models/best_model_300.t7 filter=lfs diff=lfs merge=lfs -text +thirdparty/learning3d/pretrained/exp_masknet2/models/best_model_400.t7 filter=lfs diff=lfs merge=lfs -text +thirdparty/learning3d/pretrained/exp_masknet2/models/best_model_500.t7 filter=lfs diff=lfs merge=lfs -text +thirdparty/learning3d/pretrained/exp_pcn/models/best_model.t7 filter=lfs diff=lfs merge=lfs -text +thirdparty/learning3d/pretrained/exp_pnlk/models/best_model_snap.t7 filter=lfs diff=lfs merge=lfs -text +thirdparty/learning3d/pretrained/exp_pnlk/models/best_model.t7 filter=lfs diff=lfs merge=lfs -text +thirdparty/learning3d/pretrained/exp_pnlk/models/best_ptnet_model.t7 filter=lfs diff=lfs merge=lfs -text +thirdparty/learning3d/pretrained/exp_prnet/models/best_model.t7 filter=lfs diff=lfs merge=lfs -text +thirdparty/learning3d/pretrained/exp_prnet/models/model.99.t7 filter=lfs diff=lfs merge=lfs -text +thirdparty/learning3d/utils/lib/build/lib.linux-x86_64-3.5/pointnet2_cuda.cpython-35m-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..5db2e578952da5a23547168952a19f67007d7b6d --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +__pycache__/ +*:*Zone.Identifier +.ipynb_checkpoints/ +*.ipynb + +checkpoints/ +data/ +results/ +registration_plys/ +logs/ +notebooks/ +kernels/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..187b932c190fa0630d1e731511236406aaf3511c --- /dev/null +++ b/README.md @@ -0,0 +1,251 @@ + + +

+ +

R3PM-Net: Real-time, Robust, Real-world Point Matching Network

+

AI4RWC@CVPRW 2026 - Oral Presentation

+

Paper | Project Page | Dataset

+
+

+

+

Figure 1. Overview of the R3PM-Net Architecture. R3PM-Net employs a global-aware feature extraction module with shared weights to learn geometric similarities across a full receptive field.

+ +## Introduction + +R3PM-Net is a lightweight, global-aware, object-level point matching network designed to bridge the gap between approaches trained and evaluated on clean, dense, synthetic and real-world industrial point cloud data by prioritizing both generalizability and real-time efficiency. + +

+

Figure 2. Examples of R3PM-Net performance on the Sioux-Cranfield dataset.

+ +### Datasets + +We propose two datasets; **Sioux-Cranfield** and **Sioux-Scans**, to address the gap between synthetic datasets and real-world industrial data. + +

+ + + + + +
+ +
+ Sioux-Cranfield +
+ +
+ Sioux-Scans +
+

+

Figure 3. CAD models of the Sioux-Cranfield dataset (Left). The first six belong to the Cranfield Assembly benchmark and the rest are contributions of this paper (Sioux dataset). Sioux-Scans point cloud data (Right). Target (blue) and Source (yellow) point clouds for seven distinct objects.

+ +## Environment Setup + +```bash +# 1. Create environment +conda env create -f environment.yml +conda activate r3pm_net + +# Optionally, install the dependencies and run manually: +pip install -e . +``` + +To run the evaluations, please refer to each method's repo to set up the environment: +[Predator](https://github.com/prs-eth/OverlapPredator), +[GeoTransformer](https://github.com/qinzheng93/geotransformer), +[LoGDesc](https://github.com/karim416/LoGDesc), and +[RegTR](https://github.com/yewzijian/regtr). + +Everything must be installed into the **same** conda enviromnet. + +## Data Preparation + +### ModelNet40 + +Download the dataset from [ModelNet40](http://modelnet.cs.princeton.edu/ModelNet40.zip) and extract it to: + +``` +data/ModelNet40 +``` + +To save time, download the downsampled ModelNet40 test set from [ModelNet40_Downsampled](https://huggingface.co/datasets/YasiiKB/R3PM-Net/blob/main/down_sampled_modelnet40.zip) and put it in: + +``` +data/down_sampled_modelnet40 +``` + +### Sioux-Cranfield + +Download the dataset from [Sioux_Cranfiled](https://huggingface.co/datasets/YasiiKB/R3PM-Net/blob/main/sioux_cranfield.zip) and put it in: + +``` +data/sioux_cranfield +``` + +### Sioux-Scans + +Download the dataset from [Sioux_Scans](https://huggingface.co/datasets/YasiiKB/R3PM-Net/blob/main/sioux_scans.zip) and put it in: + +``` +data/sioux_scans +``` + +### Fine-tune + +Download the pickle files (.pkl) from [here](https://huggingface.co/datasets/YasiiKB/R3PM-Net/blob/main/simulators.zip) and put them in: + +``` +data/simulators +``` +These pickle files are created from a subset of the Sioux-Cranfield containing the "teeth", "cube", "lime" and "lego" CAD models. There are 320 point cloud pairs, with 80-20 train-test split. + +Optionally, to create your own datasets, use the scripts in `dataloader`, refering to the README file in that directory. + +## Pre-trained Models + +Please download the pretrained model of each method from their repo (links provided above) and follow their instructions as to where to put them. + +We use RPMNet's pre-trained model (*clean-trained*) for our Zero-shot version. Download it from [here](https://github.com/vinits5/learning3d/tree/master/pretrained/exp_rpmnet/models) and put it in: + +``` +checkpoints/ +``` + +*Note:* You need to fine-tune the model yourself (see bleow) to get the fine-tuned weights which then you can put in the same directory. + +## Folder Structure + +```text +r3pm_net/ +├── assets/ +├── config/ +│ ├── default.yaml # Training defaults +│ └── eval.yaml # Paths for evaluation scripts +├── checkpoints/ # Pre-trained models' weights +├── data/ +│ ├── down_sampled_modelnet40/ +│ ├── ModelNet40/ +│ ├── sioux_cranfield/ +│ └── sioux_scans/ +├── dataloader/ # Dataset dict generation & loaders +├── logs/ # Experiment logs +├── r3pm_net/ # Core package (model, feature extractor, config) +├── scripts/ # SLURM/Bash and evaluation scripts +│ ├── eval_modelnet40.py +│ ├── eval_sioux_cranfield.py +│ ├── eval_sioux_scans.py +│ ├── modelnet40.sh +│ ├── sioux_cranfield.sh +│ └── sioux_scans.sh +├── src/ +│ └── train.py # Training +├── thirdparty/learning3d/ # learning3d (RPMNet, losses, ops, …) +├── tools/ # Registration eval, metrics, visualization +├── environment.yml +├── pyproject.toml +└── README.md +``` + +## Train + +To train the model using `data/simulators` or your own dataset run: +```bash +python src/train.py +``` + +## Evaluation + +Scripts are provided in `scripts/` to reproduce results. + +**ModelNet40** + +```bash +bash scripts/modelnet40.sh +``` + +**Sioux-Cranfield** + +```bash +bash scripts/sioux_cranfield.sh +``` + +**Sioux-Scans** +This evaluates the proposed hybrid Coarse-to-Fine Registration approach. + +```bash +bash scripts/sioux_scans.sh +``` + +### Manual Execution + +For example for evaluation on `Sioux-Cranfield`, run: + +```bash +python scripts/eval_sioux_cranfield.py +``` + +## Results +*IMPORTANT NOTE: Unfortunately, we cannot release the feature-extraction model and the fine-tuned weights. Therefore, to re-poduce these results you need to implement the feature extractor (based on the paper) and fine-tune it with the provided data.* + +### ModelNet40 + + +| Method | RRE [°] ↓ | RTE [cm] ↓ | CD [cm] ↓ | Fitness ↑ | In. RMSE [cm] ↓ | Time [s] ↓ | +| ------------------- | ----------------- | ----------------- | ----------------- | ----------------- | ------------------ | ----------------- | +| RPMNet | 30.898 | **0.002** | 0.153 | *0.998* | 0.094 | *0.021* | +| Predator | 7.262 | 0.028 | *0.045* | **1.000** | *0.026* | 0.071 | +| GeoTransformer | 50.357 | 0.215 | 0.255 | 0.921 | 0.101 | 0.065 | +| RegTR | **1.712** | *0.007* | **0.017** | **1.000** | **0.009** | 0.045 | +| LoGDesc | 42.762 | 0.158 | 0.183 | 0.978 | 0.097 | 0.075 | +| **R3PM-Net (ours)** | *5.198* | 0.010 | 0.052 | **1.000** | 0.029 | **0.007** | + + +> **Notes:** **Best** results are in bold; *Second-best* results are underlined. + +### Sioux-Cranfield + + +| Method | RRE [°] ↓ | RTE [cm] ↓ | CD [cm] ↓ | Fitness ↑ | In. RMSE [cm] ↓ | Time [s] ↓ | +| ------------------- | ----------------- | ----------------- | ----------------- | ----------------- | ------------------ | ----------------- | +| RPMNet | 32.217 | **0.002** | 0.160 | *0.997* | 0.098 | 0.021 | +| Predator | 16.448 | 0.044 | 0.072 | **1.000** | 0.042 | 0.071 | +| GeoTrans. | 45.582 | 0.183 | 0.297 | 0.906 | 0.111 | 0.065 | +| RegTR | **1.311** | *0.004* | **0.023** | **1.000** | **0.012** | 0.045 | +| LoGDesc | 121.224 | 0.773 | 0.692 | 0.718 | 0.224 | 0.075 | +| **R3PM-Net (ours)** | *5.451* | 0.006 | *0.054* | **1.000** | *0.030* | **0.006** | + + +### Sioux-Scans +

+ +

Figure 4. Qualitative registration results of R3PM-Net on real-world event-camera data. It successfully aligns the "teeth" and "cube" models. The fine-tuned version also solves the "lime" and "house".

+ +## Acknowledgement + +We adapted some codes from some awesome repositories including [Learning3D](https://github.com/vinits5/learning3d) and [RPMNet](https://github.com/yewzijian/RPMNet). Thanks for making the codes publicly available. + +## Citation + +If you find this repository useful, please consider citing: + +```bibtex +@misc{kashefbahrami2026r3pmnetrealtimerobustrealworld, + title={R3PM-Net: Real-time, Robust, Real-world Point Matching Network}, + author={Yasaman Kashefbahrami and Erkut Akdag and Panagiotis Meletis and Evgeniya Balmashnova and Dip Goswami and Egor Bondarau}, + year={2026}, + eprint={2604.05060}, + archivePrefix={arXiv}, + primaryClass={cs.CV}, + url={https://arxiv.org/abs/2604.05060}, +} +``` + diff --git a/assets/r3pmnet_overview.png b/assets/r3pmnet_overview.png new file mode 100644 index 0000000000000000000000000000000000000000..abaf46d13d71886ef3fc5d052c8ba4a6c434a70b --- /dev/null +++ b/assets/r3pmnet_overview.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:40dac0c28fa7d4f1213ff2ab93493bfe30afc9ccc97097faa53e5e7fcdd2d057 +size 278686 diff --git a/assets/sioux_cranfield.png b/assets/sioux_cranfield.png new file mode 100644 index 0000000000000000000000000000000000000000..d13d75a84afd4258aaabd23729d0b0d0d505d74a --- /dev/null +++ b/assets/sioux_cranfield.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8b68d27b5ac14b43a2b814d2ad8d599786d5c4187ce90bf498fb9a270011f180 +size 204151 diff --git a/assets/sioux_scans.png b/assets/sioux_scans.png new file mode 100644 index 0000000000000000000000000000000000000000..a5217537f4756c434324753a2a040c17008cc1de --- /dev/null +++ b/assets/sioux_scans.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fbd242ea53dfb82b967be8d3f1f9aa34090b72696034e0d7284760f75f82ad62 +size 428742 diff --git a/assets/success_cases.png b/assets/success_cases.png new file mode 100644 index 0000000000000000000000000000000000000000..e7cea858ea7ec2ba49c8b697047baef219b503c3 --- /dev/null +++ b/assets/success_cases.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bd2cf529c20d11f3b04b345a69746635517ea37d1964650fc47b13394f8448a1 +size 678952 diff --git a/assets/teaser.png b/assets/teaser.png new file mode 100644 index 0000000000000000000000000000000000000000..54799664ea89147a9535db4f662bdadce69876a4 --- /dev/null +++ b/assets/teaser.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a20019ddf1e712d331fd56d251111b9b37f062c908409134aaf621b6758cef58 +size 1017997 diff --git a/config/default.yaml b/config/default.yaml new file mode 100644 index 0000000000000000000000000000000000000000..160e3eaa17bb9bfdb48c6501149e056658a3dc3e --- /dev/null +++ b/config/default.yaml @@ -0,0 +1,24 @@ +# Default training and data paths for R3PM-Net + +exp_name: exp_r3pmnet +eval: false +save_dir: "" + +fine_tune_feature_extractor: tune +transfer_weights: "" # Optional: leave empty to skip loading +emb_dims: 1024 +symfn: max + +seed: 1234 +workers: 4 +batch_size: 5 +epochs: 2 +start_epoch: 0 +optimizer: Adam +resume: "" +pretrained: "" +device: cuda:0 + +# Pickled Registration Dataset dicts (keys: template, source, transformation) +train_dict_path: data/simulators/data_dict_train.pkl +test_dict_path: data/simulators/data_dict_test.pkl diff --git a/config/eval.yaml b/config/eval.yaml new file mode 100644 index 0000000000000000000000000000000000000000..68cf00ebe0d95d12c9ac4046a35fb358e832cb49 --- /dev/null +++ b/config/eval.yaml @@ -0,0 +1,28 @@ +# Paths for scripts/evaluation (loaded by scripts/*.py). + +data_root: data +pretrained_rpmnet_dir: checkpoints + +modelnet40: + dataset_path: data/ModelNet40 + cache_dir: data/down_sampled_modelnet40 + +sioux: + base_dir: data + +methods: + geotransformer: + root: GeoTransformer + exp_subdir: GeoTransformer/experiments/geotransformer.modelnet.rpmnet.stage4.gse.k3.max.oacl.stage2.sinkhorn + weights_path: GeoTransformer/weights/geotransformer-modelnet.pth.tar + predator: + root: OverlapPredator + config_path: OverlapPredator/configs/test/modelnet.yaml + weights_path: null + logdesc: + root: LoGDesc + weights_path: LoGDesc/pre-trained/best_model.pth + regtr: + root: RegTR + ckpt_path: RegTR/trained_models/modelnet/ckpt/model-best.pth + config_path: RegTR/trained_models/modelnet/config.yaml diff --git a/dataloader/README.md b/dataloader/README.md new file mode 100644 index 0000000000000000000000000000000000000000..11d3c7bb58a13f18bfcd2808091a238a46231da6 --- /dev/null +++ b/dataloader/README.md @@ -0,0 +1,71 @@ +# Dataloaders + +This directory contains scripts to generate Simulated datasets for training and testing. +It uses functionalities from `tools` folder to: + +`generate_dataset` + +- load and downsample data +- compute normals if needed +- apply random transformations +- add augmentations (noise, outliers and occlusion) +- save point clouds + +**Note: Two input point clouds must have same length.** + +`generate_dataset_dict` + +- save generated dataset in dictionaries suitable to train models (following Learning3d requirments) +- checks dimensions (to meet Learning3d requirments) + +`combine_dataset_dict` + +- shuffle and combine all generated dictionaries into one +- split train and test sets + +## How to generate datasets? + +Modify the `args.txt` file to contain the correct paths and other specifications e.g. downsampling rate, noise level, etc. Other default arguments in `data_dict_generator.py` can also be changed. + +1. Generate transformed target point clouds + GT transforms +Change `--action in dataloader/args.txt` to `generate_dataset` and run: +``` +python dataloader/data_dict_generator.py @dataloader/args.txt +``` + +2. Generate train/test .pkl dicts +Change `--action in dataloader/args.txt` to `generate_dataset_dict`, then run the above script again. + +3. Combine multiple object dicts +Set `--action combine_dataset_dict` and run again to get train and test `dict.pkl` files. + +### Manual Run (without args.txt) +Optinally you can manually run: +``` +python dataloader/data_dict_generator.py \ + --pcdPath /path/to/source_scan.pcd \ + --cadPath /path/to/object.stl \ + --name teeth \ + --action generate_dataset \ + --every_k_points 100 \ + --num_transformation 50 \ + --angles 0 90 180 \ + --translation_range -1 1 \ + --index 0 \ + --noise_level 0 \ + --outlier_level 0 \ + --outlier_bounds -0.05 0.05 \ + --occ_level 0 \ + --save_path data/simulators +``` +then: +``` +python dataloader/data_dict_generator.py \ + --pcdPath /path/to/source_scan.pcd \ + --cadPath /path/to/object.stl \ + --name teeth \ + --action generate_dataset_dict \ + --dataset_size 50 \ + --index 0 \ + --save_path data/simulators +``` \ No newline at end of file diff --git a/dataloader/__init__.py b/dataloader/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..6915b8082d93d3dade461358b8a4580c9d5ab940 --- /dev/null +++ b/dataloader/__init__.py @@ -0,0 +1 @@ +# Dataset loaders and generators diff --git a/dataloader/args.txt b/dataloader/args.txt new file mode 100644 index 0000000000000000000000000000000000000000..a9fef85e7e2b57f7829561b5be3fb5a6650eb063 --- /dev/null +++ b/dataloader/args.txt @@ -0,0 +1,14 @@ +--pcdPath data/sioux_scans/teeth_clean.ply +--cadPath data/sioux_cranfield/teeth.stl +--action combine_dataset_dict +--name teeth +--every_k_points 100 +--num_transformation 50 +--angles 0 90 180 +--translation_range -1 1 +--dataset_size 50 +--index 0 +--noise_level 0 +--outlier_level 0 +--outlier_bounds -0.05 0.05 +--occ_level 0 \ No newline at end of file diff --git a/dataloader/data_dict_generator.py b/dataloader/data_dict_generator.py new file mode 100644 index 0000000000000000000000000000000000000000..3931cbcd40bf2a661ea2a336644b4f54d860944d --- /dev/null +++ b/dataloader/data_dict_generator.py @@ -0,0 +1,119 @@ +import argparse +import copy +import logging +import os +import shlex +import sys +from pathlib import Path + +import numpy as np + +_REPO_ROOT = Path(__file__).resolve().parents[1] +if str(_REPO_ROOT) not in sys.path: + sys.path.insert(0, str(_REPO_ROOT)) + +from tools import data +from dataloader.dataset_generator import combine_dataset_dict, generate_dataset, generate_dataset_dict + +# Configure logging +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') + +def main(): + # Set up the argument parser + parser = argparse.ArgumentParser(description='Automate dataset generation and processing.') + + # Define arguments (change these as needed) + parser.add_argument('--pcdPath', type=str, required=True, help='Path to the PCD file') + parser.add_argument('--cadPath', type=str, required=True, help='Path to the CAD file') + parser.add_argument('--action', type=str, choices=['generate_dataset', 'generate_dataset_dict', 'combine_dataset_dict'], required=True, help='Action to perform') + parser.add_argument('--compute_normals', action='store_true', help='Flag to compute normals') + parser.add_argument('--every_k_points', type=int, default=1, help='Sampling rate for points') + parser.add_argument('--save', action='store_true', help='Flag to save the generated dataset') + parser.add_argument( + '--save_path', + type=str, + default='data/simulators', + help='Directory to save generated datasets (relative to repo root if not absolute)', + ) + parser.add_argument('--name', type=str, required=True, help='Name identifier for the dataset (e.g., teeth, cube, etc.)') + + # Additional parameters for dataset generation (change these as needed) + parser.add_argument('--num_transformation', type=int, default=50, help='Number of transformations') + parser.add_argument('--angles', type=int, nargs='+', default=list(range(0, 360, 10)), help='Rotation angles') + parser.add_argument('--translation_range', type=float, nargs=2, default=(-1, 1), help='Translation range') + parser.add_argument('--dataset_size', type=int, default=400, help='Size of the dataset to generate') + parser.add_argument('--index', type=int, default=0, help='Index for dataset generation') + parser.add_argument('--noise_level', type=float, default=0, help='Noise level') + parser.add_argument('--outlier_level', type=float, default=0, help='Outlier level') + parser.add_argument('--outlier_bounds', type=float, nargs=2, default=(-10, 10), help='Outlier bounds') + parser.add_argument('--occ_level', type=float, default=0, help='Occlusion level') + + # Parse the arguments + + # Check if an argument file is being used + if sys.argv[1].startswith('@'): + args_file = sys.argv[1][1:] # Strip the '@' from the filename + with open(args_file, 'r') as file: + # Read and split arguments from the file + args = parser.parse_args(shlex.split(file.read())) + else: + args = parser.parse_args() + + # Print out the arguments to verify + print(vars(args)) + + # Load the data + np.random.seed(42) + if args.compute_normals: + _, cad, _, cad_normals = data.load_data(args.pcdPath, args.cadPath, every_k_points=args.every_k_points, same_length=True, compute_normals=True) + suffix = '_with_normals' + else: + _, cad = data.load_data(args.pcdPath, args.cadPath, every_k_points=args.every_k_points, same_length=True) + cad_normals = None + suffix = '' + source = copy.deepcopy(cad) + + rp = Path(args.save_path) + if not rp.is_absolute(): + rp = _REPO_ROOT / args.save_path + ROOT_DIR = str(rp.resolve()) + if not ROOT_DIR.endswith(os.sep): + ROOT_DIR += os.sep + + # Perform the selected action + if args.action == 'generate_dataset': + logging.info('Generating dataset...') + generate_dataset(source, args.pcdPath, args.cadPath, args.num_transformation, args.angles, args.translation_range, args.index, args.noise_level, args.outlier_level, args.outlier_bounds, args.occ_level, save_dir=ROOT_DIR) + + elif args.action == 'generate_dataset_dict': + logging.info('Generating dataset dictionary...') + output_train_file = f'{ROOT_DIR}data_dict_train_{args.name}{suffix}.pkl' + output_test_file = f'{ROOT_DIR}data_dict_test_{args.name}{suffix}.pkl' + generate_dataset_dict(source, args.dataset_size, args.index, output_train_file, output_test_file, cad_normals) + + elif args.action == 'combine_dataset_dict': + logging.info('Combining dataset dictionaries...') + train_files = [ + f'{ROOT_DIR}data_dict_train_teeth{suffix}.pkl' + # f'{ROOT_DIR}data_dict_train_elephant{suffix}.pkl', + # f'{ROOT_DIR}data_dict_train_house{suffix}.pkl', + # f'{ROOT_DIR}data_dict_train_shoe{suffix}.pkl' + ] + + test_files = [ + f'{ROOT_DIR}data_dict_test_teeth{suffix}.pkl' + # f'{ROOT_DIR}data_dict_test_elephant{suffix}.pkl', + # f'{ROOT_DIR}data_dict_test_house{suffix}.pkl', + # f'{ROOT_DIR}data_dict_test_shoe{suffix}.pkl' + ] + + output_train_file = f'{ROOT_DIR}data_dict_train_{suffix}.pkl' + output_test_file = f'{ROOT_DIR}data_dict_test_{suffix}.pkl' + + combine_dataset_dict(train_files, test_files, output_train_file, output_test_file) + + else: + logging.warning('No valid action selected.') + +if __name__ == '__main__': + main() diff --git a/dataloader/dataset_generator.py b/dataloader/dataset_generator.py new file mode 100644 index 0000000000000000000000000000000000000000..793a190d4df67de12b4f3ef1380e6aedc78acd21 --- /dev/null +++ b/dataloader/dataset_generator.py @@ -0,0 +1,258 @@ +import copy +import os +import pickle +import random +import sys +from pathlib import Path + +import numpy as np +import open3d as o3 + +_REPO_ROOT = Path(__file__).resolve().parents[1] +if str(_REPO_ROOT) not in sys.path: + sys.path.insert(0, str(_REPO_ROOT)) + +from tools import augmentation, data, transformations + +_SIM_DATA = _REPO_ROOT / "data" / "simulators" +''' +This module provides functions to generate a dataset of point clouds with random transformations, with options for noise, outliers, and occlusions. +It also includes functions to check the shape of the data and to generate a data dictionary for training and testing, +and a function to combine multiple dataset dictionaries. +''' + +def generate_dataset(pcd, pcdPath, cadPath, num_transformation, angles, translation_range, index, noise_level = 0, outlier_level = 0, outlier_bounds = (-10, 10), occ_level = 0, save_dir=None): + ''' + A function to generate a dataset of point clouds with random transformations. + + Args: + pcd (open3d.geometry.PointCloud): The source point cloud + pcdPath (str): The path to the source point cloud + cadPath (str): The path to the target point cloud + num_transformation (int): The number of transformations to generate + angles (numpy.ndarray): The range of angles for the random transformations + translation_range (tuple): The range of translations for the random transformations + index (int): The index to start saving the generated dataset + noise_level (float): The level of noise to add to the point clouds + outlier_level (float): The level of outliers to add to the point clouds + occ_level (float): The level of occlusions to add to the point clouds + save (bool): A flag to save the generated dataset + + Returns: + None + ''' + np.random.seed(42) + target_list = [] + gt_transformation_list = [] + + for i in range(num_transformation): + # Generate random gt transformation + x_angle= np.random.uniform(angles[0], angles[-1], size=1) + y_angle= np.random.uniform(angles[0], angles[-1], size=1) + z_angle= np.random.uniform(angles[0], angles[-1], size=1) + gt_transformation = transformations.create_transformation(x_angle, y_angle, z_angle, translation_range) + + target = copy.deepcopy(pcd) + target.transform(gt_transformation) + + if noise_level != 0: + target = augmentation.apply_noise(target, noise_level) + print('Noise applied') + + if outlier_level != 0 or occ_level != 0: + _, another_cad = data.load_data(pcdPath, cadPath, every_k_points=1) + target = copy.deepcopy(another_cad).transform(gt_transformation) + if occ_level != 0: + target, _ = augmentation.apply_occlusion(target, occ_level) + print('Occlusion applied') + if outlier_level != 0: + target = augmentation.add_outliers(target, outlier_level, outlier_lowerbound=outlier_bounds[0], outlier_upperbound=outlier_bounds[1]) + print('Outliers applied') + + # randomly take points away from target to get to same length as source + if len(target.points) >= len(pcd.points): + np.random.seed(42) + target_points = np.asarray(target.points) + indices = np.random.choice(len(target_points), 1441, replace=False) # change len(source.points) to a specific num if you want to have a fixed number of points + sampled_points = target_points[indices] + target.points = o3.utility.Vector3dVector(sampled_points) + else: + print('Target has fewer points than source and can\'t be downsampled to the same length.') + + print(f'size of source and target: {len(pcd.points)}, {len(target.points)}') + target_list.append(target) + gt_transformation_list.append(gt_transformation) + + # Save the generated dataset + if save_dir is not None: + if not os.path.exists(save_dir): + os.makedirs(save_dir) + + for i, (target, transformation) in enumerate(zip(target_list, gt_transformation_list)): + target_path = os.path.join(save_dir, f"target_{i+index}.pcd") + transformation_path = os.path.join(save_dir, f"transformation_{i+index}.npy") + o3.io.write_point_cloud(target_path, target) + np.save(transformation_path, transformation) + +def check_shape(data, expected_shape_3d, expected_shape_6d): + return data.shape == expected_shape_3d or data.shape == expected_shape_6d + +def generate_dataset_dict(source, dataset_size, index, output_train_file_path, output_test_file_path, source_normals = None): + ''' + This function shuffles the dataset and generates a data_dict for the training and testing data following the pattern acceptable to Learning3D. + + Args: + source (open3d.geometry.PointCloud): The source point cloud + dataset_size (int): The size of the dataset + + Returns: + None + ''' + np.random.seed(42) + transformed_pcds = [] + gt_transformations = [] + + # Load the transformed point clouds and ground truth transformations + for i in range(index,index+dataset_size): + transformed_pcd = o3.io.read_point_cloud(str(_SIM_DATA / f"target_{i}.pcd")) + gt_transformation = np.load(str(_SIM_DATA / f"transformation_{i}.npy")) + + if source_normals is not None: # we also need target normals + M = np.linalg.inv(gt_transformation).T + target_normals = np.dot(source_normals, M[:3,:3]) # transformed_normals = normals * (transformation)^-1.T + transformed_points = np.concatenate((np.asarray(transformed_pcd.points), target_normals), axis=1) + else: + transformed_points = np.asarray(transformed_pcd.points).astype(np.float32) + + transformed_pcds.append(transformed_points) + gt_transformations.append(gt_transformation) + + # Shuffle the transformed point clouds and ground truth transformations in the same way + temp = list(zip(transformed_pcds, gt_transformations)) + random.shuffle(temp) + transformed_pcds, gt_transformations = zip(*temp) + + # Convert lists to numpy arrays + transformed_pcds_np = np.array(transformed_pcds) + gt_transformations_np = np.array(gt_transformations) + + if source_normals is not None: + source = np.concatenate((np.asarray(source.points), source_normals), axis=1) + else: + source = np.asarray(source.points).astype(np.float32) + + data_dict = { + 'template': np.tile(source, (dataset_size, 1, 1)), + 'source': transformed_pcds_np, + 'transformation': gt_transformations_np + } + + # Split the data_dict into training and testing data_dict + train_size = int(0.8 * dataset_size) + test_size = dataset_size - train_size + num_points = len(source) + + data_dict_train = {} + data_dict_test = {} + for key in data_dict.keys(): + data_dict_train[key] = data_dict[key][0:train_size] + data_dict_test[key] = data_dict[key][train_size:] + + assert set(data_dict_train.keys()) == {'template', 'source', 'transformation'} + assert set(data_dict_test.keys()) == {'template', 'source', 'transformation'} + + expected_shape_3d_train = (train_size, num_points, 3) + expected_shape_6d_train = (train_size, num_points, 6) + + assert check_shape(data_dict_train['template'], expected_shape_3d_train, expected_shape_6d_train), f"Expected shape: {expected_shape_3d_train} or {expected_shape_6d_train}, but got {data_dict_train['template'].shape}" + assert check_shape(data_dict_train['source'], expected_shape_3d_train, expected_shape_6d_train), f"Expected shape: {expected_shape_3d_train} or {expected_shape_6d_train}, but got {data_dict_train['source'].shape}" + assert data_dict_train['transformation'].shape == (train_size, 4, 4), f"Expected shape: {(train_size, 4, 4)}, but got {data_dict_train['transformation'].shape}" + + expected_shape_3d_test = (test_size, num_points, 3) + expected_shape_6d_test = (test_size, num_points, 6) + + assert check_shape(data_dict_test['template'], expected_shape_3d_test, expected_shape_6d_test), f"Expected shape: {expected_shape_3d_test} or {expected_shape_6d_test}, but got {data_dict_test['template'].shape}" + assert check_shape(data_dict_test['source'], expected_shape_3d_test, expected_shape_6d_test), f"Expected shape: {expected_shape_3d_test} or {expected_shape_6d_test}, but got {data_dict_test['source'].shape}" + assert data_dict_test['transformation'].shape == (test_size, 4, 4), f"Expected shape: {(test_size, 4, 4)}, but got {data_dict_test['transformation'].shape}" + + with open(output_train_file_path, 'wb') as f: + pickle.dump(data_dict_train, f) + print(f"train_dict saved to {output_train_file_path}") + + with open(output_test_file_path, 'wb') as f: + pickle.dump(data_dict_test, f) + print(f"test_dict saved to {output_test_file_path}") + + +def combine_dataset_dict(train_files, test_files, output_train_file_path, output_test_file_path): + ''' + Combine and shuffle dictionaries from multiple files. + + Args: + train_files (list of str): List of file paths to training dictionaries. + test_files (list of str): List of file paths to testing dictionaries. + output_train_file (str): Output file path for the combined training dictionary. + output_test_file (str): Output file path for the combined testing dictionary. + ''' + + # Load the dictionaries from the .pkl files + train_dicts = [pickle.load(open(file, 'rb')) for file in train_files] + test_dicts = [pickle.load(open(file, 'rb')) for file in test_files] + + # Combine the dictionaries + combined_train_dict = {} + combined_test_dict = {} + + for key in train_dicts[0].keys(): + combined_train_dict[key] = np.concatenate([d[key] for d in train_dicts], axis=0) + combined_test_dict[key] = np.concatenate([d[key] for d in test_dicts], axis=0) + + # Shuffle + train_combined_list = list(zip(combined_train_dict['template'], combined_train_dict['source'], combined_train_dict['transformation'])) + test_combined_list = list(zip(combined_test_dict['template'], combined_test_dict['source'], combined_test_dict['transformation'])) + + random.shuffle(train_combined_list) + random.shuffle(test_combined_list) + + combined_train_dict['template'], combined_train_dict['source'], combined_train_dict['transformation'] = zip(*train_combined_list) + combined_test_dict['template'], combined_test_dict['source'], combined_test_dict['transformation'] = zip(*test_combined_list) + + # Convert back to numpy arrays + combined_train_dict['template'] = np.array(combined_train_dict['template']) + combined_train_dict['source'] = np.array(combined_train_dict['source']) + combined_train_dict['transformation'] = np.array(combined_train_dict['transformation']) + + combined_test_dict['template'] = np.array(combined_test_dict['template']) + combined_test_dict['source'] = np.array(combined_test_dict['source']) + combined_test_dict['transformation'] = np.array(combined_test_dict['transformation']) + + # Checks + train_size = len(combined_train_dict['source']) + test_size = len(combined_test_dict['source']) + num_points = combined_train_dict['source'].shape[1] + + assert set(combined_train_dict.keys()) == {'template', 'source', 'transformation'} + assert set(combined_test_dict.keys()) == {'template', 'source', 'transformation'} + + expected_shape_3d_train = (train_size, num_points, 3) + expected_shape_6d_train = (train_size, num_points, 6) + + assert check_shape(combined_train_dict['template'], expected_shape_3d_train, expected_shape_6d_train), f"Expected shape: {expected_shape_3d_train} or {expected_shape_6d_train}, but got {combined_train_dict['template'].shape}" + assert check_shape(combined_train_dict['source'], expected_shape_3d_train, expected_shape_6d_train), f"Expected shape: {expected_shape_3d_train} or {expected_shape_6d_train}, but got {combined_train_dict['source'].shape}" + assert combined_train_dict['transformation'].shape == (train_size, 4, 4), f"Expected shape: {(train_size, 4, 4)}, but got {combined_train_dict['transformation'].shape}" + + expected_shape_3d_test = (test_size, num_points, 3) + expected_shape_6d_test = (test_size, num_points, 6) + + assert check_shape(combined_test_dict['template'], expected_shape_3d_test, expected_shape_6d_test), f"Expected shape: {expected_shape_3d_test} or {expected_shape_6d_test}, but got {combined_test_dict['template'].shape}" + assert check_shape(combined_test_dict['source'], expected_shape_3d_test, expected_shape_6d_test), f"Expected shape: {expected_shape_3d_test} or {expected_shape_6d_test}, but got {combined_test_dict['source'].shape}" + assert combined_test_dict['transformation'].shape == (test_size, 4, 4), f"Expected shape: {(test_size, 4, 4)}, but got {combined_test_dict['transformation'].shape}" + + # Save the dictionaries + with open(output_train_file_path, 'wb') as f: + pickle.dump(combined_train_dict, f) + print(f"combined_train_dict saved to {output_train_file_path}") + + with open(output_test_file_path, 'wb') as f: + pickle.dump(combined_test_dict, f) + print(f"combined_test_dict saved to {output_train_file_path}") diff --git a/dataloader/user_data.py b/dataloader/user_data.py new file mode 100644 index 0000000000000000000000000000000000000000..50bc563120f6e4ade584105f7908b4c38401ba8e --- /dev/null +++ b/dataloader/user_data.py @@ -0,0 +1,127 @@ +import os +import numpy as np +import torch + +class ClassificationData: + def __init__(self, data_dict): + self.data_dict = data_dict + self.pcs = self.find_attribute('pcs') + self.labels = self.find_attribute('labels') + self.check_data() + + def find_attribute(self, attribute): + try: + attribute_data = self.data_dict[attribute] + except: + print("Given data directory has no key attribute \"{}\"".format(attribute)) + return attribute_data + + def check_data(self): + assert 1 < len(self.pcs.shape) < 4, "Error in dimension of point clouds! Given data dimension: {}".format(self.pcs.shape) + assert 0 < len(self.labels.shape) < 3, "Error in dimension of labels! Given data dimension: {}".format(self.labels.shape) + + if len(self.pcs.shape)==2: self.pcs = self.pcs.reshape(1, -1, 3) + if len(self.labels.shape) == 1: self.labels = self.labels.reshape(1, -1) + + assert self.pcs.shape[0] == self.labels.shape[0], "Inconsistency in the number of point clouds and number of ground truth labels!" + + + def __len__(self): + return self.pcs.shape[0] + + def __getitem__(self, index): + return torch.tensor(self.pcs[index]).float(), torch.from_numpy(self.labels[idx]).type(torch.LongTensor) + + +class RegistrationData: + def __init__(self, data_dict): + self.data_dict = data_dict + self.template = self.find_attribute('template') + self.source = self.find_attribute('source') + self.transformation = self.find_attribute('transformation') + self.check_data() + + # def find_attribute(self, attribute): + # try: + # attribute_data = self.data[attribute] + # except: + # print("Given data directory has no key attribute \"{}\"".format(attribute)) + # return attribute_data + + def find_attribute(self, attribute): + attribute_data = None + if attribute in self.data_dict: + attribute_data = self.data_dict[attribute] + else: + print("Given data directory has no key attribute \"{}\"".format(attribute)) + return attribute_data + + def check_data(self): + assert 1 < len(self.template.shape) < 4, "Error in dimension of point clouds! Given data dimension: {}".format(self.template.shape) + assert 1 < len(self.source.shape) < 4, "Error in dimension of point clouds! Given data dimension: {}".format(self.source.shape) + assert 1 < len(self.transformation.shape) < 4, "Error in dimension of transformations! Given data dimension: {}".format(self.transformation.shape) + + if len(self.template.shape)==2: self.template = self.template.reshape(1, -1, 3) + if len(self.source.shape)==2: self.source = self.source.reshape(1, -1, 3) + if len(self.transformation.shape) == 2: self.transformation = self.transformation.reshape(1, 4, 4) + + assert self.template.shape[0] == self.source.shape[0], "Inconsistency in the number of template and source point clouds!" + assert self.source.shape[0] == self.transformation.shape[0], "Inconsistency in the number of transformation and source point clouds!" + + def __len__(self): + return self.template.shape[0] + + def __getitem__(self, index): + return torch.tensor(self.template[index]).float(), torch.tensor(self.source[index]).float(), torch.tensor(self.transformation[index]).float() + + +class FlowData: + def __init__(self, data_dict): + self.data_dict = data_dict + self.frame1 = self.find_attribute('frame1') + self.frame2 = self.find_attribute('frame2') + self.flow = self.find_attribute('flow') + self.check_data() + + def find_attribute(self, attribute): + try: + attribute_data = self.data[attribute] + except: + print("Given data directory has no key attribute \"{}\"".format(attribute)) + return attribute_data + + def check_data(self): + assert 1 < len(self.frame1.shape) < 4, "Error in dimension of point clouds! Given data dimension: {}".format(self.frame1.shape) + assert 1 < len(self.frame2.shape) < 4, "Error in dimension of point clouds! Given data dimension: {}".format(self.frame2.shape) + assert 1 < len(self.flow.shape) < 4, "Error in dimension of flow! Given data dimension: {}".format(self.flow.shape) + + if len(self.frame1.shape)==2: self.frame1 = self.frame1.reshape(1, -1, 3) + if len(self.frame2.shape)==2: self.frame2 = self.frame2.reshape(1, -1, 3) + if len(self.flow.shape) == 2: self.flow = self.flow.reshape(1, -1, 3) + + assert self.frame1.shape[0] == self.frame2.shape[0], "Inconsistency in the number of frame1 and frame2 point clouds!" + assert self.frame2.shape[0] == self.flow.shape[0], "Inconsistency in the number of flow and frame2 point clouds!" + + def __len__(self): + return self.frame1.shape[0] + + def __getitem__(self, index): + return torch.tensor(self.frame1[index]).float(), torch.tensor(self.frame2[index]).float(), torch.tensor(self.flow[index]).float() + + +class UserData: + def __init__(self, application, data_dict): + self.application = application + + if self.application == 'classification': + self.data_class = ClassificationData(data_dict) + elif self.application == 'registration': + self.data_class = RegistrationData(data_dict) + elif self.application == 'flow_estimation': + self.data_class = FlowData(data_dict) + + def __len__(self): + return len(self.data_class) + + def __getitem__(self, index): + return self.data_class[index] diff --git a/environment.yml b/environment.yml new file mode 100644 index 0000000000000000000000000000000000000000..0284cde25a457a2bf30ae2c809ef8e7ab6411f5c --- /dev/null +++ b/environment.yml @@ -0,0 +1,16 @@ +name: r3pm_net +channels: + - conda-forge + - pytorch +dependencies: + - python=3.9 + - pip + - open3d + - pytorch + - hatchling + - ipykernel + - pip: + - tabulate + - pyyaml + - -e . + diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..468bdf99321eab994aad3a97af6b6a7e13954659 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,25 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "r3pm_net" +version = "0.1.0" +description = "R3PM-Net point cloud registration" +readme = "README.md" +requires-python = ">=3.9" +dependencies = [ + "numpy", + "torch", + "tensorboardX", + "tqdm", + "pyyaml", + "open3d", + "tabulate", +] + +[tool.hatch.build.targets.wheel] +packages = ["r3pm_net", "tools", "dataloader", "thirdparty"] + +[tool.jupytext] +formats = "ipynb,py:light" diff --git a/r3pm_net/__init__.py b/r3pm_net/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8d16516e6e1588e56147c711d74269a8d7f3a847 --- /dev/null +++ b/r3pm_net/__init__.py @@ -0,0 +1,5 @@ +"""R3PM-Net: point cloud registration with PointNet features.""" + +from .model import R3PMNet + +__all__ = ["R3PMNet"] diff --git a/r3pm_net/config_loader.py b/r3pm_net/config_loader.py new file mode 100644 index 0000000000000000000000000000000000000000..80e40f8f8dfc073645d4d6151a071d99094b8975 --- /dev/null +++ b/r3pm_net/config_loader.py @@ -0,0 +1,164 @@ +"""Load YAML training config and merge with argparse.""" + +from __future__ import annotations + +import argparse +import os +from pathlib import Path +from typing import Any, Mapping + +import yaml + +from r3pm_net.paths import REPO_ROOT + + +def _resolve_maybe_relative(path_str: str | None) -> str | None: + if path_str is None or path_str == "": + return path_str + p = Path(path_str) + if p.is_absolute(): + return str(p) + return str(REPO_ROOT / p) + + +def load_yaml_config(path: str | Path) -> dict[str, Any]: + with open(path, "r", encoding="utf-8") as f: + data = yaml.safe_load(f) + if data is None: + return {} + if not isinstance(data, Mapping): + raise ValueError(f"Config must be a mapping, got {type(data)}") + return dict(data) + + +def _extract_config_argv(argv: list[str], default_cfg: str) -> tuple[str, list[str]]: + """Return (config path for YAML, argv without --config ...).""" + path = default_cfg + out: list[str] = [] + i = 0 + while i < len(argv): + if argv[i] == "--config" and i + 1 < len(argv): + path = argv[i + 1] + i += 2 + continue + if argv[i].startswith("--config="): + path = argv[i].split("=", 1)[1] + i += 1 + continue + out.append(argv[i]) + i += 1 + return path, out + + +def parse_train_args(argv: list[str], build_parser) -> argparse.Namespace: + """Load YAML from --config (default: config/default.yaml), merge as argparse defaults, then parse CLI.""" + default_cfg = str(REPO_ROOT / "config" / "default.yaml") + cfg_path, argv_rest = _extract_config_argv(list(argv), default_cfg) + cfg = load_yaml_config(cfg_path) if Path(cfg_path).is_file() else {} + parser = build_parser(cfg_path) + if cfg: + known = { + a.dest + for a in parser._actions + if getattr(a, "dest", None) and a.dest not in ("help", argparse.SUPPRESS) + } + filtered = {k: v for k, v in cfg.items() if k in known} + parser.set_defaults(**filtered) + return parser.parse_args(argv_rest) + + +def resolve_path_args(ns: Any, path_keys: tuple[str, ...]) -> None: + """Mutate namespace: resolve listed keys to absolute paths under REPO_ROOT when relative.""" + for key in path_keys: + val = getattr(ns, key, None) + if isinstance(val, str) and val: + setattr(ns, key, _resolve_maybe_relative(val)) + + +def load_eval_yaml() -> dict[str, Any]: + """Load ``config/eval.yaml`` if present; otherwise return an empty dict.""" + path = REPO_ROOT / "config" / "eval.yaml" + if not path.is_file(): + return {} + return load_yaml_config(path) + + +def get_pretrained_rpmnet_dir() -> str: + """Directory containing ``clean-trained.pth``, ``best_model_PointNet*.t7``, etc. + + ``R3PM_NET_PRETRAINED_ROOT`` overrides ``pretrained_rpmnet_dir`` in ``config/eval.yaml``. + """ + env = os.environ.get("R3PM_NET_PRETRAINED_ROOT") + if env: + return str(Path(env).expanduser().resolve()) + cfg = load_eval_yaml() + rel = (cfg.get("pretrained_rpmnet_dir") or "checkpoints").strip() + if not rel: + rel = "checkpoints" + out = _resolve_maybe_relative(rel) + return out if out else str(REPO_ROOT / "checkpoints") + + +def get_sioux_data_root() -> str: + """Base data directory for Sioux scripts (``data`` / ``sioux_cranfield``, etc.).""" + cfg = load_eval_yaml() + sioux = cfg.get("sioux") or {} + base = sioux.get("base_dir") or cfg.get("data_root") or "data" + out = _resolve_maybe_relative(str(base).strip()) + return out if out else str(REPO_ROOT / "data") + + +def get_modelnet40_paths() -> tuple[str, str]: + """Return ``(dataset_path, cache_dir)`` for ModelNet40 evaluation.""" + cfg = load_eval_yaml() + m = cfg.get("modelnet40") or {} + ds = m.get("dataset_path", "data/ModelNet40") + cache = m.get("cache_dir", "data/down_sampled_modelnet40") + dsr = _resolve_maybe_relative(ds) + cr = _resolve_maybe_relative(cache) + return ( + dsr if dsr else str(REPO_ROOT / "data" / "ModelNet40"), + cr if cr else str(REPO_ROOT / "data" / "down_sampled_modelnet40"), + ) + + +def get_method_paths() -> dict[str, Any]: + """Return resolved path configuration for external registration methods.""" + cfg = load_eval_yaml() + methods = cfg.get("methods") or {} + out: dict[str, Any] = {} + for method_name, method_cfg in methods.items(): + if not isinstance(method_cfg, Mapping): + continue + method_out: dict[str, Any] = {} + for k, v in method_cfg.items(): + if isinstance(v, str) and v.strip(): + rv = _resolve_maybe_relative(v.strip()) + method_out[k] = rv if rv else v + else: + method_out[k] = v + out[str(method_name)] = method_out + return out + + +def get_sioux_paths() -> dict[str, Any]: + """Return Sioux eval paths from config/eval.yaml with absolute paths.""" + cfg = load_eval_yaml() + sioux = cfg.get("sioux") or {} + out: dict[str, Any] = {} + for k, v in sioux.items(): + if isinstance(v, str) and v.strip(): + rv = _resolve_maybe_relative(v.strip()) + out[k] = rv if rv else v + elif isinstance(v, list): + vals = [] + for item in v: + if isinstance(item, str) and item.strip(): + rv = _resolve_maybe_relative(item.strip()) + vals.append(rv if rv else item) + else: + vals.append(item) + out[k] = vals + else: + out[k] = v + return out diff --git a/r3pm_net/feature_extractor.py b/r3pm_net/feature_extractor.py new file mode 100644 index 0000000000000000000000000000000000000000..b357e729d8ddef066d7ee4957a4907960783a898 --- /dev/null +++ b/r3pm_net/feature_extractor.py @@ -0,0 +1,8 @@ +# Feature extractor for R3PM-Net +''' +Unfortunately, the feature extractor cannot be provided in the repository due to copyright issues. +Please implement the feature extractor for R3PM-Net as described in the paper and place it in this file. +Currently, the feature extractor is set to PPFNet (same as RPMNet). +''' +from thirdparty.learning3d.models import PPFNet +feature_extractor = PPFNet() \ No newline at end of file diff --git a/r3pm_net/model.py b/r3pm_net/model.py new file mode 100644 index 0000000000000000000000000000000000000000..65e2515c1ea5df93ee1a12c7a04a64ae7017bf32 --- /dev/null +++ b/r3pm_net/model.py @@ -0,0 +1,382 @@ +import logging +import numpy as np +import torch +import torch.nn as nn +import torch.nn.functional as F + +from thirdparty.learning3d.utils import square_distance, angle_difference +from thirdparty.learning3d.ops.transform_functions import convert2transformation + +_EPS = 1e-5 # To prevent division by zero + +class ParameterPredictionNet(nn.Module): + def __init__(self, weights_dim): + """PointNet based Parameter prediction network + + Args: + weights_dim: Number of weights to predict (excluding beta), should be something like + [3], or [64, 3], for 3 types of features + """ + + super().__init__() + + self._logger = logging.getLogger(self.__class__.__name__) + + self.weights_dim = weights_dim + + # Pointnet + self.prepool = nn.Sequential( + nn.Conv1d(4, 64, 1), + nn.GroupNorm(8, 64), + nn.ReLU(), + + nn.Conv1d(64, 64, 1), + nn.GroupNorm(8, 64), + nn.ReLU(), + + nn.Conv1d(64, 64, 1), + nn.GroupNorm(8, 64), + nn.ReLU(), + + nn.Conv1d(64, 128, 1), + nn.GroupNorm(8, 128), + nn.ReLU(), + + nn.Conv1d(128, 1024, 1), + nn.GroupNorm(16, 1024), + nn.ReLU(), + ) + self.pooling = nn.AdaptiveMaxPool1d(1) + self.postpool = nn.Sequential( + nn.Linear(1024, 512), + nn.GroupNorm(16, 512), + nn.ReLU(), + + nn.Linear(512, 256), + nn.GroupNorm(16, 256), + nn.ReLU(), + + nn.Linear(256, 2 + np.prod(weights_dim)), + ) + + self._logger.info('Predicting weights with dim {}.'.format(self.weights_dim)) + + def forward(self, x): + """ Returns alpha, beta, and gating_weights (if needed) + + Args: + x: List containing two point clouds, x[0] = src (B, J, 3), x[1] = ref (B, K, 3) + + Returns: + beta, alpha, weightings + """ + # X and Y concatenated + src_padded = F.pad(x[0], (0, 1), mode='constant', value=0) + ref_padded = F.pad(x[1], (0, 1), mode='constant', value=1) + concatenated = torch.cat([src_padded, ref_padded], dim=1) + + prepool_feat = self.prepool(concatenated.permute(0, 2, 1)) + pooled = torch.flatten(self.pooling(prepool_feat), start_dim=-2) + raw_weights = self.postpool(pooled) + + # softplus to ensure positivity + beta = F.softplus(raw_weights[:, 0]) + alpha = F.softplus(raw_weights[:, 1]) + + return beta, alpha + + + +def to_numpy(tensor): + """Wrapper around .detach().cpu().numpy() """ + if isinstance(tensor, torch.Tensor): + return tensor.detach().cpu().numpy() + elif isinstance(tensor, np.ndarray): + return tensor + else: + raise NotImplementedError + + +def se3_transform(g, a, normals=None): + """ Applies the SE3 transform + + Args: + g: SE3 transformation matrix of size ([1,] 3/4, 4) or (B, 3/4, 4) + a: Points to be transformed (N, 3) or (B, N, 3) + normals: (Optional). If provided, normals will be transformed + + Returns: + transformed points of size (N, 3) or (B, N, 3) + + """ + R = g[..., :3, :3] # (B, 3, 3) + p = g[..., :3, 3] # (B, 3) + + if len(g.size()) == len(a.size()): + b = torch.matmul(a, R.transpose(-1, -2)) + p[..., None, :] + else: + raise NotImplementedError + b = R.matmul(a.unsqueeze(-1)).squeeze(-1) + p # No batch. Not checked + + if normals is not None: + rotated_normals = normals @ R.transpose(-1, -2) + return b, rotated_normals + + else: + return b + +def match_features(feat_src, feat_ref, metric='l2'): + """ Compute pairwise distance between features + + Args: + feat_src: (B, J, C) + feat_ref: (B, K, C) + metric: either 'angle' or 'l2' (squared euclidean) + + Returns: + Matching matrix (B, J, K). i'th row describes how well the i'th point + in the src agrees with every point in the ref. + """ + if feat_src.shape[-1] != feat_ref.shape[-1]: + if feat_src.shape[-1] > feat_ref.shape[-1]: + feat_src = feat_src[:,:,:feat_ref.shape[-1]] + elif feat_src.shape[-1] < feat_ref.shape[-1]: + feat_ref = feat_ref[:,:,:feat_src.shape[-1]] + + assert feat_src.shape[-1] == feat_ref.shape[-1] + + if metric == 'l2': + dist_matrix = square_distance(feat_src, feat_ref) + elif metric == 'angle': + feat_src_norm = feat_src / (torch.norm(feat_src, dim=-1, keepdim=True) + _EPS) + feat_ref_norm = feat_ref / (torch.norm(feat_ref, dim=-1, keepdim=True) + _EPS) + + dist_matrix = angle_difference(feat_src_norm, feat_ref_norm) + else: + raise NotImplementedError + + return dist_matrix + + +def sinkhorn(log_alpha, n_iters: int = 5, slack: bool = True, eps: float = -1) -> torch.Tensor: + """ Run sinkhorn iterations to generate a near doubly stochastic matrix, where each row or column sum to <=1 + + Args: + log_alpha: log of positive matrix to apply sinkhorn normalization (B, J, K) + n_iters (int): Number of normalization iterations + slack (bool): Whether to include slack row and column + eps: eps for early termination (Used only for handcrafted RPM). Set to negative to disable. + + Returns: + log(perm_matrix): Doubly stochastic matrix (B, J, K) + + Modified from original source taken from: + Learning Latent Permutations with Gumbel-Sinkhorn Networks + https://github.com/HeddaCohenIndelman/Learning-Gumbel-Sinkhorn-Permutations-w-Pytorch + """ + + # Sinkhorn iterations + prev_alpha = None + if slack: + zero_pad = nn.ZeroPad2d((0, 1, 0, 1)) + log_alpha_padded = zero_pad(log_alpha[:, None, :, :]) + + log_alpha_padded = torch.squeeze(log_alpha_padded, dim=1) + + for i in range(n_iters): + # Row normalization + log_alpha_padded = torch.cat(( + log_alpha_padded[:, :-1, :] - (torch.logsumexp(log_alpha_padded[:, :-1, :], dim=2, keepdim=True)), + log_alpha_padded[:, -1, None, :]), # Don't normalize last row + dim=1) + + # Column normalization + log_alpha_padded = torch.cat(( + log_alpha_padded[:, :, :-1] - (torch.logsumexp(log_alpha_padded[:, :, :-1], dim=1, keepdim=True)), + log_alpha_padded[:, :, -1, None]), # Don't normalize last column + dim=2) + + if eps > 0: + if prev_alpha is not None: + abs_dev = torch.abs(torch.exp(log_alpha_padded[:, :-1, :-1]) - prev_alpha) + if torch.max(torch.sum(abs_dev, dim=[1, 2])) < eps: + break + prev_alpha = torch.exp(log_alpha_padded[:, :-1, :-1]).clone() + + log_alpha = log_alpha_padded[:, :-1, :-1] + else: + for i in range(n_iters): + # Row normalization (i.e. each row sum to 1) + log_alpha = log_alpha - (torch.logsumexp(log_alpha, dim=2, keepdim=True)) + + # Column normalization (i.e. each column sum to 1) + log_alpha = log_alpha - (torch.logsumexp(log_alpha, dim=1, keepdim=True)) + + if eps > 0: + if prev_alpha is not None: + abs_dev = torch.abs(torch.exp(log_alpha) - prev_alpha) + if torch.max(torch.sum(abs_dev, dim=[1, 2])) < eps: + break + prev_alpha = torch.exp(log_alpha).clone() + + return log_alpha + + +def compute_rigid_transform(a: torch.Tensor, b: torch.Tensor, weights: torch.Tensor): + """Compute rigid transforms between two point sets + + Args: + a (torch.Tensor): (B, M, 3) points + b (torch.Tensor): (B, N, 3) points + weights (torch.Tensor): (B, M) + + Returns: + Transform T (B, 3, 4) to get from a to b, i.e. T*a = b + """ + + weights_normalized = weights[..., None] / (torch.sum(weights[..., None], dim=1, keepdim=True) + _EPS) + centroid_a = torch.sum(a * weights_normalized, dim=1) + centroid_b = torch.sum(b * weights_normalized, dim=1) + a_centered = a - centroid_a[:, None, :] + b_centered = b - centroid_b[:, None, :] + cov = a_centered.transpose(-2, -1) @ (b_centered * weights_normalized) + + # Compute rotation using Kabsch algorithm. Will compute two copies with +/-V[:,:3] + # and choose based on determinant to avoid flips + u, s, v = torch.svd(cov, some=False, compute_uv=True) + rot_mat_pos = v @ u.transpose(-1, -2) + v_neg = v.clone() + v_neg[:, :, 2] *= -1 + rot_mat_neg = v_neg @ u.transpose(-1, -2) + rot_mat = torch.where(torch.det(rot_mat_pos)[:, None, None] > 0, rot_mat_pos, rot_mat_neg) + assert torch.all(torch.det(rot_mat) > 0) + + # Compute translation (uncenter centroid) + translation = -rot_mat @ centroid_a[:, :, None] + centroid_b[:, :, None] + + transform = torch.cat((rot_mat, translation), dim=2) + return transform + +class R3PMNet(nn.Module): + def __init__(self, feature_model): + + super().__init__() + + self.add_slack = True + self.num_sk_iter = 5 + + self.weights_net = ParameterPredictionNet(weights_dim=[0]) + self.feat_extractor = feature_model + + def compute_affinity(self, beta, feat_distance, alpha=0.5): + """Compute logarithm of Initial match matrix values, i.e. log(m_jk)""" + if isinstance(alpha, float): + hybrid_affinity = -beta[:, None, None] * (feat_distance - alpha) + else: + hybrid_affinity = -beta[:, None, None] * (feat_distance - alpha[:, None, None]) + return hybrid_affinity + + @staticmethod + def split_normals(data): + if data.shape[2] == 6: + xyz, normals = data[:, :, :3], data[:, :, 3:6] + elif data.shape[2] == 3: + xyz, normals = data, torch.zeros(data.shape).to(data.device) + return xyz, normals + + def spam(self, xyz_template, norm_template, xyz_source, norm_source): + self.beta, self.alpha = self.weights_net([xyz_source, xyz_template]) + + try: # R3PMNET feature extractor + self.feat_source = self.feat_extractor(xyz_source) + self.feat_template = self.feat_extractor(xyz_template) + except: + self.feat_source = self.feat_extractor(xyz_source, norm_source) + self.feat_template = self.feat_extractor(xyz_template, norm_template) + + feat_distance = match_features(self.feat_source, self.feat_template) + self.affinity = self.compute_affinity(self.beta, feat_distance, alpha=self.alpha) + + # Compute weighted coordinates + log_perm_matrix = sinkhorn(self.affinity, n_iters=self.num_sk_iter, slack=self.add_slack) + self.perm_matrix = torch.exp(log_perm_matrix) + + try: # R3PMNET features + weighted_template = self.perm_matrix @ xyz_template[:,:self.perm_matrix.shape[1]] / (torch.sum(self.perm_matrix, dim=2, keepdim=True) + _EPS) + except: + weighted_template = self.perm_matrix @ xyz_template / (torch.sum(self.perm_matrix, dim=2, keepdim=True) + _EPS) + return weighted_template + + def forward(self, template, source, max_iterations: int = 1): + """Forward pass for R3PM-Net + + Args: + data: Dict containing the following fields: + 'points_src': Source points (B, J, 6) + 'points_ref': Reference points (B, K, 6) + num_iter (int): Number of iterations. Recommended to be 2 for training + + Returns: + transform: Transform to apply to source points such that they align to reference + src_transformed: Transformed source points + """ + + xyz_template, norm_template = self.split_normals(template) + xyz_source, norm_source = self.split_normals(source) + + xyz_source_t, norm_source_t = xyz_source, norm_source # a copy of source to apply transformation to + + transforms = [] + all_gamma, all_perm_matrices, all_weighted_template = [], [], [] + all_beta, all_alpha = [], [] + + for i in range(max_iterations): + weighted_template = self.spam(xyz_template, norm_template, xyz_source_t, norm_source_t) # Finding better correspondences after each iteration. + + # Compute transform and transform points + try: # R3PMNET features + transform = compute_rigid_transform(xyz_source[:,:weighted_template.shape[1]], weighted_template, weights=torch.sum(self.perm_matrix, dim=2)) + xyz_source_t, norm_source_t = se3_transform(transform.detach(), xyz_source[:,:weighted_template.shape[1]], norm_source) # Apply transformation to original source. + except: + transform = compute_rigid_transform(xyz_source_t, weighted_template, weights=torch.sum(self.perm_matrix, dim=2)) + xyz_source_t, norm_source_t = se3_transform(transform.detach(), xyz_source, norm_source) # Apply transformation to original source. + + + transforms.append(transform) + all_gamma.append(torch.exp(self.affinity)) + all_perm_matrices.append(self.perm_matrix) + all_weighted_template.append(weighted_template) + all_beta.append(to_numpy(self.beta)) + all_alpha.append(to_numpy(self.alpha)) + + est_T = convert2transformation(transforms[max_iterations-1][:, :3, :3], transforms[max_iterations-1][:, :3, 3]) + transformed_source = torch.bmm(est_T[:, :3, :3], source[:,:,:3].permute(0, 2, 1)).permute(0, 2, 1) + est_T[:, :3, 3].unsqueeze(1) + + try: # for training + result = {'est_R': est_T[:, :3, :3], # source -> template + 'est_t': est_T[:, :3, 3], # source -> template + 'est_T': est_T, # source -> template + 'r': self.feat_template - self.feat_source, + 'transformed_source': transformed_source} + except RuntimeError: + result = {'est_R': est_T[:, :3, :3], # source -> template + 'est_t': est_T[:, :3, 3], # source -> template + 'est_T': est_T, # source -> template + 'transformed_source': transformed_source} + + result['perm_matrices_init'] = all_gamma + result['perm_matrices'] = all_perm_matrices + result['weighted_template'] = all_weighted_template + result['beta'] = np.stack(all_beta, axis=0) + result['alpha'] = np.stack(all_alpha, axis=0) + result['transforms'] = transforms + + return result + + +if __name__ == '__main__': + template, source = torch.rand(10,1024,6), torch.rand(10,1024,6) + + net = R3PMNet() + result = net(template, source) + import ipdb; ipdb.set_trace() \ No newline at end of file diff --git a/r3pm_net/paths.py b/r3pm_net/paths.py new file mode 100644 index 0000000000000000000000000000000000000000..7c5ac79d75b49f8f4b07804fbab6b3c131d6ba8d --- /dev/null +++ b/r3pm_net/paths.py @@ -0,0 +1,11 @@ +"""Repository-root resolution for portable paths.""" + +from pathlib import Path + +# r3pm_net/paths.py -> parents[1] is the repository root +REPO_ROOT = Path(__file__).resolve().parents[1] + + +def repo_path(*parts: str) -> str: + """Join path segments relative to the repository root.""" + return str(REPO_ROOT.joinpath(*parts)) diff --git a/scripts/eval_modelnet40.py b/scripts/eval_modelnet40.py new file mode 100644 index 0000000000000000000000000000000000000000..6c58821761612fb768cc42c9abe6f97e59dac7d9 --- /dev/null +++ b/scripts/eval_modelnet40.py @@ -0,0 +1,335 @@ +import os +import copy +import sys +from pathlib import Path +from typing import Any + +# Repository root on PYTHONPATH (run: python scripts/test_modelnet40.py from repo root). +_REPO_ROOT = Path(__file__).resolve().parents[1] +if str(_REPO_ROOT) not in sys.path: + sys.path.insert(0, str(_REPO_ROOT)) + +import argparse +import random + +import numpy as np +import open3d as o3d +import torch +from tqdm import tqdm + +from tools import augmentation, data, l3d_helper, print_results, transformations +from tools import l3d_registration_and_evaluation, predator_registration_and_evaluation, geotransformer_registration_and_evaluation, logdesc_registration_and_evaluation, regtr_registration_and_evaluation +from r3pm_net.config_loader import get_method_paths,get_modelnet40_paths, get_pretrained_rpmnet_dir + +''' +This script evaluates the performance on the ModelNet40 test dataset. +The results are averaged ovet the dataset with 2468 samples. +All the point clouds are normalized to a sphere of radius 1. + +Augmentations: +- Transformation = Random rotation (0 - 45) and translation (-0.5 to 0.5) +- Noise = Gaussian noise with mean 0 and std deviation of 0.01 [optional] +- Outliers = with level 1 which means 2% of the points are outliers (PC size = 2040) [optional] +- Occlusion = 90000 radius which means 0.7% of the points are occluded (PC size = 1986) [optional] +''' +def set_seed(seed: int) -> None: + os.environ["PYTHONHASHSEED"] = str(seed) + os.environ["CUBLAS_WORKSPACE_CONFIG"] = ":4096:8" + + random.seed(seed) + np.random.seed(seed) + torch.manual_seed(seed) + torch.cuda.manual_seed_all(seed) + + torch.backends.cudnn.benchmark = False + torch.backends.cudnn.deterministic = True + torch.use_deterministic_algorithms(True) + +# arguments +parser = argparse.ArgumentParser(description="ModelNet40 R3PM-Net evaluation") +parser.add_argument("--seed", type=int, default=42, help="random seed (default: 42)") + +args = parser.parse_args() +set_seed(args.seed) +method_paths = get_method_paths() + +pretrained_base_dir = get_pretrained_rpmnet_dir() +_path_zs = os.path.join(pretrained_base_dir, "clean-trained.pth") +_path_ft = os.path.join(pretrained_base_dir, "best_model_PointNet.t7") #TODO: CHANGE + +def fix_off_file(file_path): + with open(file_path, 'r') as f: + lines = f.readlines() + + if lines[0].startswith("OFF") and len(lines[0].strip().split()) > 1: + header = lines[0].strip() + new_header = "OFF\n" + header[3:] + "\n" + lines = [new_header] + lines[1:] + + with open(file_path, 'w') as f: + f.writelines(lines) + print(f"Fixed: {file_path}") + +def load_modelnet40_test_data(dataset_path, num_points=2000): + test_data = [] + test_labels = [] + categories = os.listdir(dataset_path) + for label, category in enumerate(tqdm(categories, desc="Loading Data")): + test_dir = os.path.join(dataset_path, category, 'test') + if not os.path.exists(test_dir): + continue + for file in tqdm(os.listdir(test_dir), desc=f"Processing {category} Category", leave=False): + if file.endswith('.off'): + file_path = os.path.join(test_dir, file) + mesh = o3d.io.read_triangle_mesh(file_path) + point_cloud = mesh.sample_points_poisson_disk(number_of_points=num_points) + test_data.append(point_cloud) + test_labels.append(label) + + return test_data, test_labels, categories + +# download from http://modelnet.cs.princeton.edu/ModelNet40.zip unzip and put the path in the config/eval.yaml +dataset_path, save_dir = get_modelnet40_paths() +test_data_path = os.path.join(save_dir, "test_data.npy") +test_labels_path = os.path.join(save_dir, "test_labels.npy") +categories_path = os.path.join(save_dir, "categories.npy") + +os.makedirs(save_dir, exist_ok=True) + +# Check if data already exists +if os.path.exists(test_data_path) and os.path.exists(test_labels_path) and os.path.exists(categories_path): + print("Loading existing test data...") + test_data_np = np.load(test_data_path, allow_pickle=True) + test_labels = np.load(test_labels_path) + categories = np.load(categories_path) + print("Done! Testing the models...") +else: + print("Loading and processing ModelNet40 test data...") + # Fix all .OFF files in the dataset + for root, _, files in os.walk(dataset_path): + for file in files: + if file.endswith(".off"): + fix_off_file(os.path.join(root, file)) + + test_data, test_labels, categories = load_modelnet40_test_data(dataset_path) + + test_data_np = [data.normalize_pc(pc, return_as_np = True) for pc in test_data] + + np.save(test_data_path, test_data_np) + np.save(test_labels_path, test_labels) + np.save(categories_path, categories) + print("Test data saved!") + +# Initialize arrays to store results +rpm_results_all = [] +predator_results_all = [] +geotransformer_results_all = [] +logdesc_results_all = [] +regtr_results_all = [] +r3pm_net_results_all = [] +tuned_r3pm_net_results_all = [] + +rpm_reg_results_all = [] +predator_reg_results_all = [] +geotransformer_reg_results_all = [] +logdesc_reg_results_all = [] +regtr_reg_results_all = [] +r3pm_net_reg_results_all = [] +tuned_r3pm_net_reg_results_all = [] + +all_sources = [] +all_targets = [] +all_angles ={} + +# Reconstruct Open3D PointCloud objects from saved npy arrays +test_data = [o3d.geometry.PointCloud(o3d.utility.Vector3dVector(points)) for points in test_data_np] + +noise_level = 0 +outlier_level = 0 +outlier_lowerbound = -0.5 +outlier_upperbound = 0.5 +# occlusion_level = 90000 # Higher value means less occlusion +occlusion_level = 0 # Higher value means less occlusion + + +# set arguments for models +rpm_args = l3d_helper.options(modelName="RPMNet") +rpm_args.pretrained = _path_zs + +# OverlapPredator (used by Predator runner) +predator_cfg = method_paths.get("predator", {}) +predator_root = predator_cfg.get("root") +predator_config_path = predator_cfg.get("config_path") +predator_weights_path = predator_cfg.get("weights_path") + +# GeoTransformer +geo_cfg = method_paths.get("geotransformer", {}) +geotransformer_root = geo_cfg.get("root") +geotransformer_exp_subdir = geo_cfg.get("exp_subdir") +geotransformer_weights_path = geo_cfg.get("weights_path") + +# LoGDesc +logdesc_cfg = method_paths.get("logdesc", {}) +logdesc_root = logdesc_cfg.get("root") +logdesc_weights_path = logdesc_cfg.get("weights_path") + +# RegTR +regtr_cfg = method_paths.get("regtr", {}) +regtr_root = regtr_cfg.get("root") +regtr_ckpt_path = regtr_cfg.get("ckpt_path") +regtr_config_path = regtr_cfg.get("config_path") + +# R3PM-Net (ours) - ZS - no training +r3pm_net_args = l3d_helper.options(modelName="R3PMNet") +r3pm_net_args.pretrained = _path_zs + +# R3PM-Net (ours) - FT +tuned_r3pm_net_args = l3d_helper.options(modelName="R3PMNet") +tuned_r3pm_net_args.pretrained = _path_ft + +for i, item in enumerate(tqdm(test_data, desc="Testing methods")): + + # Simulate data + x_angle = int(random.uniform(0, 45)) + y_angle = int(random.uniform(0, 45)) + z_angle = int(random.uniform(0, 45)) + translation_range = (-0.5, 0.5) + gt_transformation = transformations.create_transformation(x_angle, y_angle, z_angle, translation_range) + source = copy.deepcopy(item) + + target = copy.deepcopy(item).transform(gt_transformation) + + # Apply augmentations + noisy_source = copy.deepcopy(source) + if noise_level != 0: + noisy_source = augmentation.apply_noise(noisy_source, noise_level) + if outlier_level != 0: + noisy_source = augmentation.add_outliers(noisy_source, outlier_level, outlier_lowerbound, outlier_upperbound) + if occlusion_level != 0: + noisy_source, _ = augmentation.apply_occlusion(noisy_source, occlusion_level) + if len(noisy_source.points) < 1024: # cannot be smaller than embedding dims in config/default.yaml + noisy_source = copy.deepcopy(source) + noisy_source = augmentation.apply_noise(noisy_source, noise_level) + noisy_source, _ = augmentation.apply_occlusion(noisy_source, occlusion_level * 100) + assert len(noisy_source.points) >= 1024, "Noisy source point cloud has less than 1024 points." + + # RPMNet + rpm_results_pc, rpm_results = l3d_registration_and_evaluation.l3d_reg_and_eval( + noisy_source, target, 'rpmnet', gt_transformation, rpm_args) + rpm_results_all.append(rpm_results) + rpm_reg_results_all.append(rpm_results_pc) + + # OverlapPredator + predator_results_pc, predator_results = predator_registration_and_evaluation.predator_reg_and_eval( + noisy_source, + target, + gt_transformation=gt_transformation, + predator_root=predator_root, + config_path=predator_config_path, + weights_path=predator_weights_path, + ransac_n_points=1000, + ransac_distance_threshold=0.05, + ransac_n=3, + sampling="prob", + mutual=False, + input_num_points=1024, + ) + predator_results_all.append(predator_results) + predator_reg_results_all.append(predator_results_pc) + + # GeoTransformer (ModelNet) + geotransformer_results_pc, geotransformer_results = geotransformer_registration_and_evaluation.geotransformer_reg_and_eval( + noisy_source, + target, + gt_transformation=gt_transformation, + geotransformer_root=geotransformer_root, + exp_subdir=geotransformer_exp_subdir, + weights_path=geotransformer_weights_path, + ) + geotransformer_results_all.append(geotransformer_results) + geotransformer_reg_results_all.append(geotransformer_results_pc) + + # LoGDesc + logdesc_results_pc, logdesc_results = logdesc_registration_and_evaluation.logdesc_reg_and_eval( + noisy_source, + target, + gt_transformation=gt_transformation, + logdesc_root=logdesc_root, + weights_path=logdesc_weights_path, + max_keypoints=768, + num_points_per_sample=128, + sample_radius=0.3, + topk_matches=128, + use_kpt=False, + ) + logdesc_results_all.append(logdesc_results) + logdesc_reg_results_all.append(logdesc_results_pc) + + # RegTR (ModelNet) + regtr_results_pc, regtr_results = regtr_registration_and_evaluation.regtr_reg_and_eval( + noisy_source, + target, + gt_transformation=gt_transformation, + regtr_root=regtr_root, + ckpt_path=regtr_ckpt_path, + config_path=regtr_config_path, + ) + regtr_results_all.append(regtr_results) + regtr_reg_results_all.append(regtr_results_pc) + + # R3PM-Net (ours) - no training + r3pm_net_results_pc, r3pm_net_results = l3d_registration_and_evaluation.l3d_reg_and_eval( + noisy_source, target, 'r3pmnet', gt_transformation, r3pm_net_args) + r3pm_net_results_all.append(r3pm_net_results) + r3pm_net_reg_results_all.append(r3pm_net_results_pc) + + # R3PM-Net (ours) (Tuned on 4 sioux data) + tuned_r3pm_net_results_pc, tuned_r3pm_net_results = l3d_registration_and_evaluation.l3d_reg_and_eval( + noisy_source, target, 'r3pmnet', gt_transformation, tuned_r3pm_net_args) + tuned_r3pm_net_results_all.append(tuned_r3pm_net_results) + tuned_r3pm_net_reg_results_all.append(tuned_r3pm_net_results_pc) + + + all_sources.append(noisy_source) + all_targets.append(target) + all_angles[i] = { + "x_angle": x_angle, + "y_angle": y_angle, + "z_angle": z_angle, + "translation": gt_transformation[:3, 3] + } + +# Convert results to numpy arrays for easier manipulation +rpm_results_all = np.array(rpm_results_all) +predator_results_all = np.array(predator_results_all) +geotransformer_results_all = np.array(geotransformer_results_all) +logdesc_results_all = np.array(logdesc_results_all) +regtr_results_all = np.array(regtr_results_all) +r3pm_net_results_all = np.array(r3pm_net_results_all) +tuned_r3pm_net_results_all = np.array(tuned_r3pm_net_results_all) + +rpm_mean_results = np.mean(rpm_results_all, axis=0) +predator_mean_results = np.mean(predator_results_all, axis=0) +geotransformer_mean_results = np.mean(geotransformer_results_all, axis=0) +logdesc_mean_results = np.mean(logdesc_results_all, axis=0) +regtr_mean_results = np.mean(regtr_results_all, axis=0) +r3pm_net_mean_results = np.mean(r3pm_net_results_all, axis=0) +tuned_r3pm_net_mean_results = np.mean(tuned_r3pm_net_results_all, axis=0) + +# Print the results +metric_names = ['mean_rmse', 'mean_rotation_error', 'mean_translation_error', + 'mean_computation_time', 'mean_cd', 'mean_error', + 'mean_fitness', 'mean_inlier_rmse'] + +reports = { + "RPMNet": dict(zip(metric_names, rpm_mean_results)), + "Predator": dict(zip(metric_names, predator_mean_results)), + "GeoTransformer": dict(zip(metric_names, geotransformer_mean_results)), + "LoGDesc": dict(zip(metric_names, logdesc_mean_results)), + "RegTR": dict(zip(metric_names, regtr_mean_results)), + "R3PM-Net (ours) (ZS)": dict(zip(metric_names, r3pm_net_mean_results)), + "R3PM-Net (ours) (FT)": dict(zip(metric_names, tuned_r3pm_net_mean_results)), +} + +# Print the table +print_results.print_table(reports) \ No newline at end of file diff --git a/scripts/eval_sioux_cranfield.py b/scripts/eval_sioux_cranfield.py new file mode 100644 index 0000000000000000000000000000000000000000..c99e99a0f721023e08f7eda487d7d46ffb7137a8 --- /dev/null +++ b/scripts/eval_sioux_cranfield.py @@ -0,0 +1,302 @@ +import os +import copy +import open3d as o3d +import numpy as np +from tqdm import tqdm +import sys +from pathlib import Path +import torch +import random +import argparse + +_REPO_ROOT = Path(__file__).resolve().parents[1] +if str(_REPO_ROOT) not in sys.path: + sys.path.insert(0, str(_REPO_ROOT)) + +from tools import augmentation, data, l3d_helper, print_results, transformations +from tools import l3d_registration_and_evaluation, predator_registration_and_evaluation, geotransformer_registration_and_evaluation, logdesc_registration_and_evaluation, regtr_registration_and_evaluation +from r3pm_net.config_loader import get_method_paths, get_pretrained_rpmnet_dir, get_sioux_data_root, get_sioux_paths +''' +This script evaluates the performance on a Sioux-Cranfield dataset +Cranfield dataset from: https://github.com/Menthy-Denayer/PCR_CAD_Model_Alignment_Comparison/tree/main/datasets +''' +def set_seed(seed: int) -> None: + os.environ["PYTHONHASHSEED"] = str(seed) + os.environ["CUBLAS_WORKSPACE_CONFIG"] = ":4096:8" + + random.seed(seed) + np.random.seed(seed) + torch.manual_seed(seed) + torch.cuda.manual_seed_all(seed) + + torch.backends.cudnn.benchmark = False + torch.backends.cudnn.deterministic = True + torch.use_deterministic_algorithms(True) + +# arguments +parser = argparse.ArgumentParser(description="Sioux-Cranfield R3PM-Net evaluation") +parser.add_argument("--seed", type=int, default=42, help="random seed (default: 42)") +args = parser.parse_args() +set_seed(args.seed) + +base_dir = get_sioux_data_root() +sioux_cfg = get_sioux_paths() +method_paths = get_method_paths() + +pretrained_base_dir = get_pretrained_rpmnet_dir() +_path_zs = os.path.join(pretrained_base_dir, "clean-trained.pth") +_path_ft = os.path.join(pretrained_base_dir, "best_model_PointNet.t7") #TODO: CHANGE + +# Paths to the CAD models +cad_dir_made = os.path.join(base_dir, 'sioux_cranfield') + +cad_paths = [os.path.join(cad_dir_made, 'Base-Top_Plate.stl'), + os.path.join(cad_dir_made, 'Pendulum.stl'), + os.path.join(cad_dir_made, 'Round-Peg.stl'), + os.path.join(cad_dir_made, 'Separator.stl'), + os.path.join(cad_dir_made, 'Shaft-New.stl'), + os.path.join(cad_dir_made, 'Square-Peg.stl'), + os.path.join(cad_dir_made, 'elephant.stl'), + os.path.join(cad_dir_made, 'house.stl'), + os.path.join(cad_dir_made, 'shoe.stl')] + +# Test parameters +num_tests = 25 +angles = list(range(0, 45)) +translation_range = (-0.5, 0.5) +np.random.seed(42) + +# Augmentation parameters +noise_level = 0 +outlier_level = 0 +outlier_lowerbound = -0.5 +outlier_upperbound = 0.5 +# occlusion_level = 9000 # Higher value means less occlusion +occ_level = 0 + +# Make dataset +sources = [] +targets = [] +x_angles = [] +y_angles = [] +z_angles = [] +gt_transformations = [] + +for cadPath in tqdm (cad_paths, desc="Preparing Sioux-Cranfield Dataset", total=len(cad_paths)): + + num_points = 2000 + # Load the data + mesh = o3d.io.read_triangle_mesh(cadPath) + cad = mesh.sample_points_poisson_disk(number_of_points=num_points) # modify to a suitable number of points + normalized_point_cloud = data.normalize_pc(cad) + source = copy.deepcopy(normalized_point_cloud) + + for test in range(num_tests): + # Data simulation + x_angle= np.random.uniform(angles[0], angles[-1], size=1) + y_angle= np.random.uniform(angles[0], angles[-1], size=1) + z_angle= np.random.uniform(angles[0], angles[-1], size=1) + gt_transformation = transformations.create_transformation(x_angle, y_angle, z_angle, translation_range) + target = copy.deepcopy(normalized_point_cloud).transform(gt_transformation) + + # Data augmentation + if occ_level == 0 and noise_level == 0 and outlier_level == 0: + noisy_source = copy.deepcopy(source) + + # Noise + Occlusion + elif occ_level != 0 and noise_level != 0: + noisy_source_noise = augmentation.apply_noise(source, noise_level) + noisy_source, _ = augmentation.apply_occlusion(noisy_source_noise, occ_level) + if len(noisy_source.points) < 1024: # Handle excessive occlusion + source = copy.deepcopy(target).transform(gt_transformation) + noisy_source_noise = augmentation.apply_noise(source, noise_level) + noisy_source, _ = augmentation.apply_occlusion(noisy_source_noise, occ_level * 1.5) + + # Noise + Outlier + elif noise_level != 0 and outlier_level != 0: + noisy_source_noise = augmentation.apply_noise(source, noise_level) + noisy_source = augmentation.add_outliers(noisy_source_noise, outlier_level, outlier_lowerbound=-0.5, outlier_upperbound=0.5) + + # Noise + Outlier + Occlusion + elif occ_level != 0 and noise_level != 0 and outlier_level != 0: + noisy_source_noise = augmentation.apply_noise(source, noise_level) + noisy_source, _ = augmentation.apply_occlusion(noisy_source_noise, occ_level) + if len(noisy_source.points) < 1024: # Handle excessive occlusion + source = copy.deepcopy(target).transform(gt_transformation) + noisy_source_noise = augmentation.apply_noise(source, noise_level) + noisy_source, _ = augmentation.apply_occlusion(noisy_source_noise, occ_level * 1.5) + noisy_source = augmentation.add_outliers(noisy_source, outlier_level, outlier_lowerbound=-0.5, outlier_upperbound=0.5) + + # collect dataset in lists + sources.append(noisy_source) + targets.append(target) + x_angles.append(x_angle) + y_angles.append(y_angle) + z_angles.append(z_angle) + gt_transformations.append(gt_transformation) + +# Initialize arrays to store results +rpm_results_all = [] +predator_results_all = [] +geotransformer_results_all = [] +logdesc_results_all = [] +regtr_results_all = [] +r3pm_net_results_all = [] +tuned_r3pm_net_results_all = [] + +rpm_reg_results_all = [] +predator_reg_results_all = [] +geotransformer_reg_results_all = [] +logdesc_reg_results_all = [] +regtr_reg_results_all = [] +r3pm_net_reg_results_all = [] +tuned_r3pm_net_reg_results_all = [] + +# set arguments for models +rpm_args = l3d_helper.options(modelName="RPMNet") +rpm_args.pretrained = _path_zs + +# OverlapPredator (used by Predator runner) +predator_cfg = method_paths.get("predator", {}) +predator_root = predator_cfg.get("root") +predator_config_path = predator_cfg.get("config_path") +predator_weights_path = predator_cfg.get("weights_path") + +# GeoTransformer +geo_cfg = method_paths.get("geotransformer", {}) +geotransformer_root = geo_cfg.get("root") +geotransformer_exp_subdir = geo_cfg.get("exp_subdir") +geotransformer_weights_path = geo_cfg.get("weights_path") + +# LoGDesc +logdesc_cfg = method_paths.get("logdesc", {}) +logdesc_root = logdesc_cfg.get("root") +logdesc_weights_path = logdesc_cfg.get("weights_path") + +# RegTR +regtr_cfg = method_paths.get("regtr", {}) +regtr_root = regtr_cfg.get("root") +regtr_ckpt_path = regtr_cfg.get("ckpt_path") +regtr_config_path = regtr_cfg.get("config_path") + +# R3PM-Net (ours) - ZS - no training +r3pm_net_args = l3d_helper.options(modelName="R3PMNet") +r3pm_net_args.pretrained = _path_zs + +# R3PM-Net (ours) - FT +tuned_r3pm_net_args = l3d_helper.options(modelName="R3PMNet") +tuned_r3pm_net_args.pretrained = _path_ft + + +for i, item in enumerate(tqdm(zip(sources, targets, gt_transformations), desc="Testing methods", total=len(sources))): + + # RPMNet + rpm_results_pc, rpm_results = l3d_registration_and_evaluation.l3d_reg_and_eval( + sources[i], targets[i], 'rpmnet', gt_transformations[i], rpm_args) + rpm_results_all.append(rpm_results) + rpm_reg_results_all.append(rpm_results_pc) + + # OverlapPredator + predator_results_pc, predator_results = predator_registration_and_evaluation.predator_reg_and_eval( + sources[i], + targets[i], + gt_transformation=gt_transformations[i], + predator_root=predator_root, + config_path=predator_config_path, + weights_path=predator_weights_path, + ransac_n_points=1000, + ransac_distance_threshold=0.05, + ransac_n=3, + sampling="prob", + mutual=False, + input_num_points=1024, + ) + predator_results_all.append(predator_results) + predator_reg_results_all.append(predator_results_pc) + + # GeoTransformer (ModelNet) + geotransformer_results_pc, geotransformer_results = geotransformer_registration_and_evaluation.geotransformer_reg_and_eval( + sources[i], + targets[i], + gt_transformation=gt_transformations[i], + geotransformer_root=geotransformer_root, + exp_subdir=geotransformer_exp_subdir, + weights_path=geotransformer_weights_path, + ) + geotransformer_results_all.append(geotransformer_results) + geotransformer_reg_results_all.append(geotransformer_results_pc) + + # LoGDesc + logdesc_results_pc, logdesc_results = logdesc_registration_and_evaluation.logdesc_reg_and_eval( + sources[i], + targets[i], + gt_transformation=gt_transformations[i], + logdesc_root=logdesc_root, + weights_path=logdesc_weights_path, + max_keypoints=768, + num_points_per_sample=128, + sample_radius=0.3, + topk_matches=128, + use_kpt=False, + ) + logdesc_results_all.append(logdesc_results) + logdesc_reg_results_all.append(logdesc_results_pc) + + # RegTR (ModelNet) + regtr_results_pc, regtr_results = regtr_registration_and_evaluation.regtr_reg_and_eval( + sources[i], + targets[i], + gt_transformation=gt_transformations[i], + regtr_root=regtr_root, + ckpt_path=regtr_ckpt_path, + config_path=regtr_config_path, + ) + regtr_results_all.append(regtr_results) + regtr_reg_results_all.append(regtr_results_pc) + + # R3PM-Net (ours) - ZS - no training + r3pm_net_results_pc, r3pm_net_results = l3d_registration_and_evaluation.l3d_reg_and_eval( + sources[i], targets[i], 'r3pmnet', gt_transformations[i], r3pm_net_args) + r3pm_net_results_all.append(r3pm_net_results) + r3pm_net_reg_results_all.append(r3pm_net_results_pc) + + # R3PM-Net (ours) - FT + tuned_r3pm_net_results_pc, tuned_r3pm_net_results = l3d_registration_and_evaluation.l3d_reg_and_eval( + sources[i], targets[i], 'r3pmnet', gt_transformations[i], tuned_r3pm_net_args) + tuned_r3pm_net_results_all.append(tuned_r3pm_net_results) + tuned_r3pm_net_reg_results_all.append(tuned_r3pm_net_results_pc) + + +# Convert results to numpy arrays for easier manipulation +rpm_results_all = np.array(rpm_results_all) +predator_results_all = np.array(predator_results_all) +geotransformer_results_all = np.array(geotransformer_results_all) +logdesc_results_all = np.array(logdesc_results_all) +regtr_results_all = np.array(regtr_results_all) +r3pm_net_results_all = np.array(r3pm_net_results_all) +tuned_r3pm_net_results_all = np.array(tuned_r3pm_net_results_all) + +rpm_mean_results = np.mean(rpm_results_all, axis=0) +predator_mean_results = np.mean(predator_results_all, axis=0) +geotransformer_mean_results = np.mean(geotransformer_results_all, axis=0) +logdesc_mean_results = np.mean(logdesc_results_all, axis=0) +regtr_mean_results = np.mean(regtr_results_all, axis=0) +r3pm_net_mean_results = np.mean(r3pm_net_results_all, axis=0) +tuned_r3pm_net_mean_results = np.mean(tuned_r3pm_net_results_all, axis=0) + +# Print the results +metric_names = ['mean_rmse', 'mean_rotation_error', 'mean_translation_error', + 'mean_computation_time', 'mean_cd', 'mean_error', + 'mean_fitness', 'mean_inlier_rmse'] + +reports = { + "RPMNet": dict(zip(metric_names, rpm_mean_results)), + "Predator": dict(zip(metric_names, predator_mean_results)), + "GeoTransformer": dict(zip(metric_names, geotransformer_mean_results)), + "LoGDesc": dict(zip(metric_names, logdesc_mean_results)), + "RegTR": dict(zip(metric_names, regtr_mean_results)), + "R3PM-Net (ours) (ZS)": dict(zip(metric_names, r3pm_net_mean_results)), + "R3PM-Net (ours) (FT)": dict(zip(metric_names, tuned_r3pm_net_mean_results)),} + +# Print the table +print_results.print_table(reports) diff --git a/scripts/eval_sioux_scans.py b/scripts/eval_sioux_scans.py new file mode 100644 index 0000000000000000000000000000000000000000..7d8b48540e533a87c4c4c6a405283baa360f4a36 --- /dev/null +++ b/scripts/eval_sioux_scans.py @@ -0,0 +1,341 @@ +import os +import copy +import argparse +import numpy as np +import random +import torch +from tabulate import tabulate +from tqdm import tqdm +import sys +from pathlib import Path + +_REPO_ROOT = Path(__file__).resolve().parents[1] +if str(_REPO_ROOT) not in sys.path: + sys.path.insert(0, str(_REPO_ROOT)) + +from tools import data, l3d_helper, visualization +from tools import icp_registration_and_evaluation, l3d_registration_and_evaluation, predator_registration_and_evaluation, geotransformer_registration_and_evaluation, logdesc_registration_and_evaluation, regtr_registration_and_evaluation +from r3pm_net.config_loader import get_pretrained_rpmnet_dir, get_sioux_data_root, get_method_paths + +''' +This script is used to evaluate the performance of the pipeline with R3PM-Net as global and GICP as local registeration. + +The script takes the following arguments: +--local_reg: the local registration method to be used. +--seed: random seed for python/numpy/torch. The default is 42. +--verbose: if set to True, the results will be printed in a table format. The default is False. +''' +def set_seed(seed: int) -> None: + os.environ["PYTHONHASHSEED"] = str(seed) + os.environ["CUBLAS_WORKSPACE_CONFIG"] = ":4096:8" + + random.seed(seed) + np.random.seed(seed) + torch.manual_seed(seed) + torch.cuda.manual_seed_all(seed) + + torch.backends.cudnn.benchmark = False + torch.backends.cudnn.deterministic = True + torch.use_deterministic_algorithms(True) + + +# arguments +parser = argparse.ArgumentParser(description="Choosing local registration method") +parser.add_argument( + "--local_reg", type=str, default="gicp", help="local registration: gicp or freg" +) +parser.add_argument("--seed", type=int, default=42, help="random seed (default: 42)") + +args = parser.parse_args() +set_seed(args.seed) +print(f"Using {args.local_reg} for local registration") + +def analyze_results(results: dict, recall_threshold = 1, rmse_threshold = 0.053, verbose = False): # change the default values to your needs + table = [] + fail_count = 0 + success_count = 0 + for object, values in results.items(): + row = [object] + list(values) + if round(row[2], 3) < recall_threshold or round(row[3], 3) > rmse_threshold: + status = 'failed' + fail_count += 1 + print(f'No match for {object}! Try a different method. If the issue persists, please check the data.') + else: + status = 'success' + success_count += 1 + print(f'Found match for {object}!') + row.append(status) + table.append(row) + + if verbose: + print(tabulate(table, headers=['Object', 'Chamfer Distance', 'Reg. Recall', 'Inlier RMSE', 'Computation Time', 'Status'], tablefmt='grid')) + print(f"Success rate: {success_count / (success_count + fail_count) * 100:.2f}%") + + return table + +def show_successful_resutls(table, sources, targets, pc_results, method_name = None): + for i in range (len(table)): + if table[i][-1] == 'success': + # visualization.plot_point_cloud(sources[i], targets[i], list(pc_results.values())[i]) # uncomment if below visualization does not work + visualization.draw_registration_result(targets[i], list(pc_results.values())[i], np.eye(4), method_name) + +def main(): + base_dir = get_sioux_data_root() + scan_dir = os.path.join(base_dir, 'sioux_scans') + cad_dir = os.path.join(base_dir, 'sioux_cranfield') + + pcd_paths = [ os.path.join(scan_dir,'teeth_clean.ply'), + os.path.join(scan_dir,'lime_clean.ply'), + os.path.join(scan_dir,'cube_clean.ply'), + os.path.join(scan_dir,'lego_clean.ply'), + os.path.join(scan_dir,'elephant_clean.ply'), + os.path.join(scan_dir,'house_clean.ply'), + os.path.join(scan_dir,'shoe_clean.ply')] + + cad_paths = [ os.path.join(cad_dir,'teeth.stl'), + os.path.join(cad_dir,'lime.stl'), + os.path.join(cad_dir,'cube.stl'), + os.path.join(cad_dir,'lego.stl'), + os.path.join(cad_dir,'elephant.stl'), + os.path.join(cad_dir,'house.stl'), + os.path.join(cad_dir,'shoe.stl')] + + # Initialize lists and dictionaries to store results + rpm_net_results = {} + rpm_net_pc_results = {} + predator_results = {} + predator_pc_results = {} + geotransformer_results = {} + geotransformer_pc_results = {} + logdesc_results = {} + logdesc_pc_results = {} + regtr_results = {} + regtr_pc_results = {} + r3pm_net_results = {} + r3pm_net_pc_results ={} + tuned_r3pm_net_results = {} + tuned_r3pm_net_pc_results = {} + subset_tuned_r3pm_net_results = {} + subset_tuned_r3pm_net_pc_results = {} + + sources = [] + targets = [] + + pretrained_base_dir = get_pretrained_rpmnet_dir() + method_paths = get_method_paths() + _path_zs = os.path.join(pretrained_base_dir, "clean-trained.pth") + _path_ft = os.path.join(pretrained_base_dir, "best_model_PointNet2.t7") #TODO: CHANGE + _path_ft_sub = os.path.join(pretrained_base_dir, "best_model_PointNet_subset.t7") #TODO: CHANGE + + # set arguments for models + rpm_args = l3d_helper.options(modelName="RPMNet") + rpm_args.pretrained = _path_zs + + # OverlapPredator (used by Predator runner) + predator_cfg = method_paths.get("predator", {}) + predator_root = predator_cfg.get("root") + predator_config_path = predator_cfg.get("config_path") + predator_weights_path = predator_cfg.get("weights_path") + + # GeoTransformer + geo_cfg = method_paths.get("geotransformer", {}) + geotransformer_root = geo_cfg.get("root") + geotransformer_exp_subdir = geo_cfg.get("exp_subdir") + geotransformer_weights_path = geo_cfg.get("weights_path") + + # LoGDesc + logdesc_cfg = method_paths.get("logdesc", {}) + logdesc_root = logdesc_cfg.get("root") + logdesc_weights_path = logdesc_cfg.get("weights_path") + + # RegTR + regtr_cfg = method_paths.get("regtr", {}) + regtr_root = regtr_cfg.get("root") + regtr_ckpt_path = regtr_cfg.get("ckpt_path") + regtr_config_path = regtr_cfg.get("config_path") + + # R3PM-Net (ours) - no training + r3pm_net_args = l3d_helper.options(modelName="R3PMNet") + r3pm_net_args.pretrained = _path_zs + + # R3PM-Net (ours) (FT) + tuned_r3pm_net_args = l3d_helper.options(modelName="R3PMNet") + tuned_r3pm_net_args.pretrained = _path_ft + + # R3PM-Net (ours) (FT) (Subset) + subset_tuned_r3pm_net_args = l3d_helper.options(modelName="R3PMNet") + subset_tuned_r3pm_net_args.pretrained = _path_ft_sub + + for pcdPath, cadPath in tqdm(zip(pcd_paths, cad_paths), desc="Registering objects", total=len(pcd_paths)): + # Define the number of points to sample from the CAD model (change this based on your data) + if 'teeth' in pcdPath: + every_k_points = 100 + key = 'teeth' + elif'lime' in pcdPath: + every_k_points = 100 + key = 'lime' + elif 'cube' in pcdPath: + every_k_points = 1 + key = 'cube' + elif 'lego' in pcdPath: + every_k_points = 10 + key = 'lego' + elif 'elephant' in pcdPath: + every_k_points = 30 + key = 'elephant' + elif 'house' in pcdPath: + every_k_points = 25 + key = 'house' + elif 'shoe' in pcdPath: + every_k_points = 15 + key = 'shoe' + else: + print("Unknown object type, using default every_k_points = 1") + every_k_points = 1 + + # Load the data + pcd, cad = data.load_data(pcdPath, cadPath, every_k_points=every_k_points) + source = copy.deepcopy(pcd) + target = copy.deepcopy(cad) + + # Normalize the point clouds + source = data.normalize_pc(source) + target = data.normalize_pc(target) + + sources.append(source) + targets.append(target) + + gt_transformation = None + + # Perform the registration + + # RPMNet + rpm_pc_result, _ = l3d_registration_and_evaluation.l3d_reg_and_eval( + source, target, 'rpmnet', gt_transformation, rpm_args) + if args.local_reg == 'gicp': + final_rpm_net_pc_result, final_rpm_net_results = icp_registration_and_evaluation.icp_reg_and_eval(rpm_pc_result, target, 'gicp', 1, np.identity(4), gt_transformation) + rpm_net_results[key] = final_rpm_net_results + rpm_net_pc_results[key] = final_rpm_net_pc_result + + # OverlapPredator + predator_results_pc, _ = predator_registration_and_evaluation.predator_reg_and_eval( + source, + target, + gt_transformation=gt_transformation, + predator_root=predator_root, + config_path=predator_config_path, + weights_path=predator_weights_path, + ransac_n_points=1000, + ransac_distance_threshold=0.05, + ransac_n=3, + sampling="prob", + mutual=False, + input_num_points=1024, + ) + if args.local_reg == 'gicp': + final_predator_pc_result, final_predator_results = icp_registration_and_evaluation.icp_reg_and_eval(predator_results_pc, target, 'gicp', 1, np.identity(4), gt_transformation) + predator_results[key] = final_predator_results + predator_pc_results[key] = final_predator_pc_result + + # GeoTransformer (ModelNet) + geotransformer_pc_result, _ = geotransformer_registration_and_evaluation.geotransformer_reg_and_eval( + source, + target, + gt_transformation=gt_transformation, + geotransformer_root=geotransformer_root, + exp_subdir=geotransformer_exp_subdir, + weights_path=geotransformer_weights_path, + ) + if args.local_reg == 'gicp': + final_geotransformer_pc_result, final_geotransformer_results = icp_registration_and_evaluation.icp_reg_and_eval(geotransformer_pc_result, target, 'gicp', 1, np.identity(4), gt_transformation) + geotransformer_results[key] = final_geotransformer_results + geotransformer_pc_results[key] = final_geotransformer_pc_result + + # LoGDesc + logdesc_pc_result, _ = logdesc_registration_and_evaluation.logdesc_reg_and_eval( + source, + target, + gt_transformation=gt_transformation, + logdesc_root=logdesc_root, + weights_path=logdesc_weights_path, + max_keypoints=768, + num_points_per_sample=128, + sample_radius=0.3, + topk_matches=128, + use_kpt=False, + ) + if args.local_reg == 'gicp': + final_logdesc_pc_result, final_logdesc_results = icp_registration_and_evaluation.icp_reg_and_eval(logdesc_pc_result, target, 'gicp', 1, np.identity(4), gt_transformation) + logdesc_results[key] = final_logdesc_results + logdesc_pc_results[key] = final_logdesc_pc_result + + # RegTR (ModelNet) + regtr_pc_result, _ = regtr_registration_and_evaluation.regtr_reg_and_eval( + source, + target, + gt_transformation=gt_transformation, + regtr_root=regtr_root, + ckpt_path=regtr_ckpt_path, + config_path=regtr_config_path, + ) + if args.local_reg == 'gicp': + final_regtr_pc_result, final_regtr_results = icp_registration_and_evaluation.icp_reg_and_eval(regtr_pc_result, target, 'gicp', 1, np.identity(4), gt_transformation) + regtr_results[key] = final_regtr_results + regtr_pc_results[key] = final_regtr_pc_result + + # R3PM-Net (ours) (ZS) + r3pm_net_pc_result, _ = l3d_registration_and_evaluation.l3d_reg_and_eval(source, target, 'r3pmnet', gt_transformation, r3pm_net_args) + if args.local_reg == 'gicp': + final_r3pm_net_pc_result, final_r3pm_net_results = icp_registration_and_evaluation.icp_reg_and_eval(r3pm_net_pc_result, target, 'gicp', 1, np.identity(4), gt_transformation) + r3pm_net_results[key] = final_r3pm_net_results + r3pm_net_pc_results[key] = final_r3pm_net_pc_result + + # R3PM-Net (ours) (FT) + tuned_r3pm_net_pc_result, _ = l3d_registration_and_evaluation.l3d_reg_and_eval(source, target, 'r3pmnet', gt_transformation, tuned_r3pm_net_args) + if args.local_reg == 'gicp': + final_tuned_r3pm_net_pc_result, final_tuned_r3pm_net_results = icp_registration_and_evaluation.icp_reg_and_eval(tuned_r3pm_net_pc_result, target, 'gicp', 1, np.identity(4), gt_transformation) + tuned_r3pm_net_results[key] = final_tuned_r3pm_net_results + tuned_r3pm_net_pc_results[key] = final_tuned_r3pm_net_pc_result + + # R3PM-Net (ours) (FT) (Subset) + subset_tuned_r3pm_net_pc_result, _ = l3d_registration_and_evaluation.l3d_reg_and_eval(source, target, 'r3pmnet', gt_transformation, subset_tuned_r3pm_net_args) + if args.local_reg == 'gicp': + final_subset_tuned_r3pm_net_pc_result, final_subset_tuned_r3pm_net_results = icp_registration_and_evaluation.icp_reg_and_eval(subset_tuned_r3pm_net_pc_result, target, 'gicp', 1, np.identity(4), gt_transformation) + subset_tuned_r3pm_net_results[key] = final_subset_tuned_r3pm_net_results + subset_tuned_r3pm_net_pc_results[key] = final_subset_tuned_r3pm_net_pc_result + + # Print the results + print("----- RPMNet: -----") + rpm_net_table = analyze_results(rpm_net_results, verbose=True) + show_successful_resutls(rpm_net_table, sources, targets, rpm_net_pc_results, 'RPMNet') + + print("----- Predator: -----") + predator_table = analyze_results(predator_results, verbose=True) + show_successful_resutls(predator_table, sources, targets, predator_pc_results, 'Predator') + + print("----- GeoTransformer: -----") + geotransformer_table = analyze_results(geotransformer_results, verbose=True) + show_successful_resutls(geotransformer_table, sources, targets, geotransformer_pc_results, 'GeoTransformer') + + print("----- LoGDesc: -----") + logdesc_table = analyze_results(logdesc_results, verbose=True) + show_successful_resutls(logdesc_table, sources, targets, logdesc_pc_results, 'LoGDesc') + + print("----- RegTR: -----") + regtr_table = analyze_results(regtr_results, verbose=True) + show_successful_resutls(regtr_table, sources, targets, regtr_pc_results, 'RegTR') + + print("----- R3PM-Net (ours) (ZS): -----") + r3pm_net_table = analyze_results(r3pm_net_results, verbose=True) + show_successful_resutls(r3pm_net_table, sources, targets, r3pm_net_pc_results, 'R3PM-Net (ours) (ZS)') + + print("----- R3PM-Net (ours) (FT): ----- ") + tuned_r3pm_net_table = analyze_results(tuned_r3pm_net_results, verbose=True) + show_successful_resutls(tuned_r3pm_net_table, sources, targets, tuned_r3pm_net_pc_results, 'R3PM-Net (ours) (FT)') + + print("----- R3PM-Net (ours) (FT) (Subset): ----- ") + subset_tuned_r3pm_net_table = analyze_results(subset_tuned_r3pm_net_results, verbose=True) + show_successful_resutls(subset_tuned_r3pm_net_table, sources, targets, subset_tuned_r3pm_net_pc_results, 'R3PM-Net (ours) (FT) (Subset)') + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/scripts/modelnet40.sh b/scripts/modelnet40.sh new file mode 100644 index 0000000000000000000000000000000000000000..4544770fd2ec72df26d295bc7ca3d60606b27209 --- /dev/null +++ b/scripts/modelnet40.sh @@ -0,0 +1,45 @@ +#!/bin/bash +#SBATCH --partition=gpu_h100 +#SBATCH --gpus=1 +#SBATCH --job-name=modelnet40 +#SBATCH --ntasks=1 +#SBATCH --time=09:00:00 +#SBATCH --output=modelnet40_output_%A.txt +#SBATCH --error=modelnet40_error_%A.txt + +# Load necessary modules (adjust based on your environment) +module purge +module load 2023 +module load CUDA/12.1.1 + +# my miniconda3 path +export PATH="$HOME/miniconda3/bin:$PATH" +unset -f conda 2>/dev/null +source "$HOME/miniconda3/etc/profile.d/conda.sh" + +# Activate the conda environment +conda activate r3pm_net + +if [[ -n "${SLURM_SUBMIT_DIR:-}" ]]; then + REPO_ROOT="$(cd "${SLURM_SUBMIT_DIR}" && pwd)" +else + REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +fi +cd "$REPO_ROOT" || { echo "ERROR: cannot cd to REPO_ROOT=${REPO_ROOT}" >&2; exit 1; } +if [[ ! -f "${REPO_ROOT}/pyproject.toml" ]]; then + echo "ERROR: REPO_ROOT=${REPO_ROOT} is not the r3pm_net tree (missing pyproject.toml)." >&2 + echo "Run: cd /path/to/r3pm_net && sbatch scripts/modelnet40.sh" >&2 + exit 1 +fi + +LOGDIR="${REPO_ROOT}/logs/slurm" +mkdir -p "$LOGDIR" +JOB_ID="${SLURM_JOB_ID:-local}" + +# seeds=(42 61 92 114 123 456 789) +seeds=(42) + +for seed in "${seeds[@]}"; do + srun python scripts/eval_modelnet40.py --seed "${seed}" \ + >"${LOGDIR}/modelnet40_job${JOB_ID}_seed${seed}.log" 2>&1 +done \ No newline at end of file diff --git a/scripts/sioux_cranfield.sh b/scripts/sioux_cranfield.sh new file mode 100644 index 0000000000000000000000000000000000000000..1201c9d12001dfd1f480599e724a0838b3c7cf47 --- /dev/null +++ b/scripts/sioux_cranfield.sh @@ -0,0 +1,46 @@ +#!/bin/bash +#SBATCH --partition=gpu_h100 +#SBATCH --gpus=1 +#SBATCH --job-name=sioux_cranfield +#SBATCH --ntasks=1 +#SBATCH --time=04:00:00 +#SBATCH --output=sioux_cranfield_output_%A.txt +#SBATCH --error=sioux_cranfield_error_%A.txt + +# Load necessary modules (adjust based on your environment) +module purge +module load 2023 +module load CUDA/12.1.1 + +# my miniconda3 path +export PATH="$HOME/miniconda3/bin:$PATH" +unset -f conda 2>/dev/null +source "$HOME/miniconda3/etc/profile.d/conda.sh" + +# Activate the conda environment +conda activate r3pm_net + + +if [[ -n "${SLURM_SUBMIT_DIR:-}" ]]; then + REPO_ROOT="$(cd "${SLURM_SUBMIT_DIR}" && pwd)" +else + REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +fi +cd "$REPO_ROOT" || { echo "ERROR: cannot cd to REPO_ROOT=${REPO_ROOT}" >&2; exit 1; } +if [[ ! -f "${REPO_ROOT}/pyproject.toml" ]]; then + echo "ERROR: REPO_ROOT=${REPO_ROOT} is not the r3pm_net tree (missing pyproject.toml)." >&2 + echo "Run: cd /path/to/r3pm_net && sbatch scripts/sioux_cranfield.sh" >&2 + exit 1 +fi + +LOGDIR="${REPO_ROOT}/logs/slurm" +mkdir -p "$LOGDIR" +JOB_ID="${SLURM_JOB_ID:-local}" + +# seeds=(42 61 92 114 123 456 789) +seeds=(42) + +for seed in "${seeds[@]}"; do + srun python scripts/eval_sioux_cranfield.py --seed "${seed}" \ + >"${LOGDIR}/sioux_cranfield_job${JOB_ID}_seed${seed}.log" 2>&1 +done \ No newline at end of file diff --git a/scripts/sioux_scans.sh b/scripts/sioux_scans.sh new file mode 100644 index 0000000000000000000000000000000000000000..92106a39584ba7ae92c735aa78ca0361318b7bf3 --- /dev/null +++ b/scripts/sioux_scans.sh @@ -0,0 +1,45 @@ +#!/bin/bash +#SBATCH --partition=gpu_h100 +#SBATCH --gpus=1 +#SBATCH --job-name=sioux_scans +#SBATCH --ntasks=1 +#SBATCH --time=01:00:00 +#SBATCH --output=sioux_scans_output_%A.txt +#SBATCH --error=sioux_scans_error_%A.txt + +# Load necessary modules (adjust based on your environment) +module purge +module load 2023 +module load CUDA/12.1.1 + +# my miniconda3 path +export PATH="$HOME/miniconda3/bin:$PATH" +unset -f conda 2>/dev/null +source "$HOME/miniconda3/etc/profile.d/conda.sh" + +# Activate the conda environment +conda activate r3pm_net + +if [[ -n "${SLURM_SUBMIT_DIR:-}" ]]; then + REPO_ROOT="$(cd "${SLURM_SUBMIT_DIR}" && pwd)" +else + REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +fi +cd "$REPO_ROOT" || { echo "ERROR: cannot cd to REPO_ROOT=${REPO_ROOT}" >&2; exit 1; } +if [[ ! -f "${REPO_ROOT}/pyproject.toml" ]]; then + echo "ERROR: REPO_ROOT=${REPO_ROOT} is not the r3pm_net tree (missing pyproject.toml)." >&2 + echo "Run: cd /path/to/r3pm_net && sbatch scripts/sioux_scans.sh" >&2 + exit 1 +fi + +LOGDIR="${REPO_ROOT}/logs/slurm" +mkdir -p "$LOGDIR" +JOB_ID="${SLURM_JOB_ID:-local}" + +# seeds=(42 61 92 114 123 456 789) +seeds=(42) + +for seed in "${seeds[@]}"; do + srun python scripts/eval_sioux_scans.py --seed "${seed}" \ + >"${LOGDIR}/sioux_scans_job${JOB_ID}_seed${seed}.log" 2>&1 +done \ No newline at end of file diff --git a/src/train.py b/src/train.py new file mode 100644 index 0000000000000000000000000000000000000000..87417939918b2f9b43206bd54835e6519369c630 --- /dev/null +++ b/src/train.py @@ -0,0 +1,366 @@ +import argparse +import os +import pickle +import sys +from pathlib import Path + +import numpy as np +import torch +from tensorboardX import SummaryWriter +from torch.utils.data import DataLoader +from tqdm import tqdm + +# Repository root on PYTHONPATH (for `python src/train.py` or srun). +_REPO_ROOT = Path(__file__).resolve().parents[1] +if str(_REPO_ROOT) not in sys.path: + sys.path.insert(0, str(_REPO_ROOT)) + +from r3pm_net.model import R3PMNet +from r3pm_net.config_loader import parse_train_args, resolve_path_args +from r3pm_net.paths import REPO_ROOT +from thirdparty.learning3d.losses import FrobeniusNormLoss, RMSEFeaturesLoss +from dataloader.user_data import UserData +from r3pm_net.feature_extractor import feature_extractor # import your feature extractor here + +def _init_(args): + Path(args.save_dir).mkdir(parents=True, exist_ok=True) + (REPO_ROOT / "checkpoints" / args.exp_name).mkdir(parents=True, exist_ok=True) + + if os.path.isfile("main.py"): + os.system("cp main.py checkpoints" + "/" + args.exp_name + "/" + "main.py.backup") + if os.path.isfile("model.py"): + os.system("cp model.py checkpoints" + "/" + args.exp_name + "/" + "model.py.backup") + + +class IOStream: + def __init__(self, path): + self.f = open(path, "a") + + def cprint(self, text): + print(text) + self.f.write(text + "\n") + self.f.flush() + + def close(self): + self.f.close() + + +def test_one_epoch(device, model, test_loader): + model.eval() + test_loss = 0.0 + count = 0 + for i, data in enumerate(tqdm(test_loader)): + template, source, igt = data + + template = template.to(device) + source = source.to(device) + igt = igt.to(device) + + output = model(template, source) + loss_val = FrobeniusNormLoss()(output["est_T"], igt) + RMSEFeaturesLoss()(output["r"]) + + test_loss += loss_val.item() + count += 1 + + test_loss = float(test_loss) / count + return test_loss + + +def test(args, model, test_loader, textio): + test_loss = test_one_epoch(args.device, model, test_loader) + textio.cprint("Validation Loss: %f" % (test_loss)) + + +def train_one_epoch(device, model, train_loader, optimizer): + model.train() + train_loss = 0.0 + count = 0 + for i, data in enumerate(tqdm(train_loader)): + template, source, igt = data + + template = template.to(device) + source = source.to(device) + igt = igt.to(device) + + output = model(template, source) + loss_val = FrobeniusNormLoss()(output["est_T"], igt) + RMSEFeaturesLoss()(output["r"]) + + optimizer.zero_grad() + loss_val.backward() + optimizer.step() + + train_loss += loss_val.item() + count += 1 + + train_loss = float(train_loss) / count + return train_loss + + +def train(args, model, train_loader, test_loader, boardio, textio, checkpoint): + Path(args.save_dir).mkdir(parents=True, exist_ok=True) + + learnable_params = filter(lambda p: p.requires_grad, model.parameters()) + if args.optimizer == "Adam": + optimizer = torch.optim.Adam(learnable_params) + else: + optimizer = torch.optim.SGD(learnable_params, lr=0.1) + + if checkpoint is not None: + optimizer.load_state_dict(checkpoint["optimizer"]) + + best_test_loss = np.inf + + for epoch in range(args.start_epoch, args.epochs): + train_loss = train_one_epoch(args.device, model, train_loader, optimizer) + test_loss = test_one_epoch(args.device, model, test_loader) + + snap = { + "epoch": epoch + 1, + "model": model.state_dict(), + "min_loss": test_loss, + "optimizer": optimizer.state_dict(), + } + + if test_loss < best_test_loss: + best_test_loss = test_loss + best_snap_path = os.path.join( + args.save_dir, "best_model_snap.t7") + best_model_path = os.path.join( + args.save_dir, "best_model.t7") + + torch.save(snap, best_snap_path) + torch.save(model.state_dict(), best_model_path) + + torch.save(snap, os.path.join(args.save_dir, "model_snap.t7")) + torch.save(model.state_dict(), os.path.join(args.save_dir, "model.t7")) + + boardio.add_scalar("Train Loss", train_loss, epoch + 1) + boardio.add_scalar("Test Loss", test_loss, epoch + 1) + boardio.add_scalar("Best Test Loss", best_test_loss, epoch + 1) + + textio.cprint( + "EPOCH:: %d, Traininig Loss: %f, Testing Loss: %f, Best Loss: %f" + % (epoch + 1, train_loss, test_loss, best_test_loss) + ) + + +def build_parser(default_config_path: str): + parser = argparse.ArgumentParser(description="Point Cloud Registration") + parser.add_argument( + "--config", + type=str, + default=default_config_path, + help="YAML file with defaults (see config/default.yaml); can be overridden on the command line", + ) + parser.add_argument( + "--exp_name", + type=str, + default="exp_r3pmnet", + metavar="N", + help="Name of the experiment", + ) + parser.add_argument("--eval", action="store_true", help="Run evaluation only (no training).") + parser.add_argument( + "--save_dir", + type=str, + default="", + help="Directory to save model checkpoints (default: checkpoints//models)", + ) + + parser.add_argument( + "--num_points", + default=1024, + type=int, + metavar="N", + help="points in point-cloud (default: 1024)", + ) + + parser.add_argument( + "--fine_tune_feature_extractor", + default="tune", + type=str, + choices=["fixed", "tune"], + help="train feature extractor (default: tune)", + ) + parser.add_argument( + "--transfer_weights", + default="", + type=str, + metavar="PATH", + help="optional path to feature extractor checkpoint", + ) + parser.add_argument( + "--symfn", + default="max", + choices=["max", "avg"], + help="symmetric function (default: max)", + ) + + parser.add_argument("--seed", type=int, default=1234) + parser.add_argument( + "-j", + "--workers", + default=4, + type=int, + metavar="N", + help="number of data loading workers (default: 4)", + ) + parser.add_argument( + "-b", + "--batch_size", + default=5, + type=int, + metavar="N", + help="mini-batch size (default: 5)", + ) + parser.add_argument( + "--epochs", + default=50, + type=int, + metavar="N", + help="number of total epochs to run", + ) + parser.add_argument( + "--start_epoch", + default=0, + type=int, + metavar="N", + help="manual epoch number (useful on restarts)", + ) + parser.add_argument( + "--optimizer", + default="Adam", + choices=["Adam", "SGD"], + metavar="METHOD", + help="name of an optimizer (default: Adam)", + ) + parser.add_argument( + "--resume", + default="", + type=str, + metavar="PATH", + help="path to latest checkpoint (default: none)", + ) + parser.add_argument( + "--pretrained", + default="", + type=str, + metavar="PATH", + help="path to pretrained full model (default: none)", + ) + parser.add_argument( + "--device", + default="cuda:0", + type=str, + metavar="DEVICE", + help="use CUDA if available", + ) + + parser.add_argument( + "--train_dict_path", + type=str, + default="data/simulators/data_dict_train.pkl", + help="Pickled training data_dict", + ) + parser.add_argument( + "--test_dict_path", + type=str, + default="data/simulators/data_dict_test.pkl", + help="Pickled test data_dict", + ) + + return parser + + +def _torch_load(path, map_location): + try: + return torch.load(path, map_location=map_location, weights_only=False) + except TypeError: + return torch.load(path, map_location=map_location) + + +def main(): + args = parse_train_args(sys.argv[1:], build_parser) + + resolve_path_args( + args, + ( + "save_dir", + "train_dict_path", + "test_dict_path", + "resume", + "pretrained", + "transfer_weights", + ), + ) + + if not args.save_dir: + args.save_dir = str(REPO_ROOT / "checkpoints" / args.exp_name / "models") + + torch.backends.cudnn.deterministic = True + torch.manual_seed(args.seed) + torch.cuda.manual_seed_all(args.seed) + np.random.seed(args.seed) + + ckpt_dir = REPO_ROOT / "checkpoints" / args.exp_name + ckpt_dir.mkdir(parents=True, exist_ok=True) + boardio = SummaryWriter(log_dir=str(ckpt_dir)) + _init_(args) + + textio = IOStream(str(ckpt_dir / "run.log")) + textio.cprint(str(args)) + + if not os.path.isfile(args.train_dict_path): + raise FileNotFoundError(f"Training dict not found: {args.train_dict_path}") + if not os.path.isfile(args.test_dict_path): + raise FileNotFoundError(f"Test dict not found: {args.test_dict_path}") + + with open(args.train_dict_path, "rb") as f: + data_dict_train = pickle.load(f) + with open(args.test_dict_path, "rb") as f: + data_dict_test = pickle.load(f) + + trainset = UserData("registration", data_dict_train) + testset = UserData("registration", data_dict_test) + train_loader = DataLoader(trainset, batch_size=args.batch_size, shuffle=False, drop_last=True, num_workers=args.workers) + test_loader = DataLoader(testset, batch_size=5, shuffle=False, drop_last=False, num_workers=args.workers) + + if not torch.cuda.is_available(): + args.device = "cpu" + args.device = torch.device(args.device) + + # feature extractor model + FEATURE_MODEL = feature_extractor + model = R3PMNet(feature_model=FEATURE_MODEL) + model = model.to(args.device) + + if args.transfer_weights and os.path.isfile(args.transfer_weights): + feat_model_dict = _torch_load(args.transfer_weights, args.device) + model.feat_extractor.load_state_dict(feat_model_dict) + + checkpoint = None + if args.resume: + assert os.path.isfile(args.resume) + checkpoint = _torch_load(args.resume, args.device) + args.start_epoch = checkpoint["epoch"] + model.load_state_dict(checkpoint["model"]) + + if args.pretrained: + assert os.path.isfile(args.pretrained) + try: + model.load_state_dict(_torch_load(args.pretrained, "cpu")) + except RuntimeError: + model_data = _torch_load(args.pretrained, "cpu") + state_dict = model_data["state_dict"] + model.load_state_dict(state_dict) + model.to(args.device) + + Path(args.save_dir).mkdir(parents=True, exist_ok=True) + + if args.eval: + test(args, model, test_loader, textio) + else: + train(args, model, train_loader, test_loader, boardio, textio, checkpoint) + +if __name__ == "__main__": + main() diff --git a/thirdparty/__init__.py b/thirdparty/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d9be4158e4074c90db04c7624772426bddf49346 --- /dev/null +++ b/thirdparty/__init__.py @@ -0,0 +1 @@ +# Namespace for vendored thirdparty.learning3d diff --git a/thirdparty/learning3d/data_utils/__init__.py b/thirdparty/learning3d/data_utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..9fb879674ab5863de1a3033ba12b0d494b85c17a --- /dev/null +++ b/thirdparty/learning3d/data_utils/__init__.py @@ -0,0 +1,4 @@ +from .dataloaders import ModelNet40Data +from .dataloaders import ClassificationData, RegistrationData, SegmentationData, FlowData, SceneflowDataset +from .dataloaders import download_modelnet40, deg_to_rad, create_random_transform +from .user_data import UserData \ No newline at end of file diff --git a/thirdparty/learning3d/data_utils/dataloaders.py b/thirdparty/learning3d/data_utils/dataloaders.py new file mode 100644 index 0000000000000000000000000000000000000000..6d672dbfaadb7733f49c6c235af447cc80d24151 --- /dev/null +++ b/thirdparty/learning3d/data_utils/dataloaders.py @@ -0,0 +1,454 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F +from torch.utils.data import Dataset +from torch.utils.data import DataLoader +import numpy as np +import os +import h5py +import subprocess +import shlex +import json +import glob +from .. ops import transform_functions, se3 +from sklearn.neighbors import NearestNeighbors +from scipy.spatial.distance import minkowski +from scipy.spatial import cKDTree +from torch.utils.data import Dataset + +def download_modelnet40(): + BASE_DIR = os.path.dirname(os.path.abspath(__file__)) + DATA_DIR = os.path.join(BASE_DIR, os.pardir, 'data') + if not os.path.exists(DATA_DIR): + os.mkdir(DATA_DIR) + if not os.path.exists(os.path.join(DATA_DIR, 'modelnet40_ply_hdf5_2048')): + www = 'https://shapenet.cs.stanford.edu/media/modelnet40_ply_hdf5_2048.zip' + zipfile = os.path.basename(www) + os.system('wget --no-check-certificate %s; unzip %s' % (www, zipfile)) + os.system('mv %s %s' % (zipfile[:-4], DATA_DIR)) + os.system('rm %s' % (zipfile)) + +def load_data(train, use_normals): + if train: partition = 'train' + else: partition = 'test' + BASE_DIR = os.path.dirname(os.path.abspath(__file__)) + DATA_DIR = os.path.join(BASE_DIR, os.pardir, 'data') + all_data = [] + all_label = [] + for h5_name in glob.glob(os.path.join(DATA_DIR, 'modelnet40_ply_hdf5_2048', 'ply_data_%s*.h5' % partition)): + f = h5py.File(h5_name) + if use_normals: data = np.concatenate([f['data'][:], f['normal'][:]], axis=-1).astype('float32') + else: data = f['data'][:].astype('float32') + label = f['label'][:].astype('int64') + f.close() + all_data.append(data) + all_label.append(label) + all_data = np.concatenate(all_data, axis=0) + all_label = np.concatenate(all_label, axis=0) + return all_data, all_label + +def deg_to_rad(deg): + return np.pi / 180 * deg + +def create_random_transform(dtype, max_rotation_deg, max_translation): + max_rotation = deg_to_rad(max_rotation_deg) + rot = np.random.uniform(-max_rotation, max_rotation, [1, 3]) + trans = np.random.uniform(-max_translation, max_translation, [1, 3]) + quat = transform_functions.euler_to_quaternion(rot, "xyz") + + vec = np.concatenate([quat, trans], axis=1) + vec = torch.tensor(vec, dtype=dtype) + return vec + +def jitter_pointcloud(pointcloud, sigma=0.04, clip=0.05): + # N, C = pointcloud.shape + sigma = 0.04*np.random.random_sample() + pointcloud += torch.empty(pointcloud.shape).normal_(mean=0, std=sigma).clamp(-clip, clip) + return pointcloud + +def farthest_subsample_points(pointcloud1, num_subsampled_points=768): + pointcloud1 = pointcloud1 + num_points = pointcloud1.shape[0] + nbrs1 = NearestNeighbors(n_neighbors=num_subsampled_points, algorithm='auto', + metric=lambda x, y: minkowski(x, y)).fit(pointcloud1[:, :3]) + random_p1 = np.random.random(size=(1, 3)) + np.array([[500, 500, 500]]) * np.random.choice([1, -1, 1, -1]) + idx1 = nbrs1.kneighbors(random_p1, return_distance=False).reshape((num_subsampled_points,)) + gt_mask = torch.zeros(num_points).scatter_(0, torch.tensor(idx1), 1) + return pointcloud1[idx1, :], gt_mask + +def uniform_2_sphere(num: int = None): + """Uniform sampling on a 2-sphere + + Source: https://gist.github.com/andrewbolster/10274979 + + Args: + num: Number of vectors to sample (or None if single) + + Returns: + Random Vector (np.ndarray) of size (num, 3) with norm 1. + If num is None returned value will have size (3,) + + """ + if num is not None: + phi = np.random.uniform(0.0, 2 * np.pi, num) + cos_theta = np.random.uniform(-1.0, 1.0, num) + else: + phi = np.random.uniform(0.0, 2 * np.pi) + cos_theta = np.random.uniform(-1.0, 1.0) + + theta = np.arccos(cos_theta) + x = np.sin(theta) * np.cos(phi) + y = np.sin(theta) * np.sin(phi) + z = np.cos(theta) + + return np.stack((x, y, z), axis=-1) + +def planar_crop(points, p_keep= 0.7): + p_keep = np.array(p_keep, dtype=np.float32) + + rand_xyz = uniform_2_sphere() + pts = points.numpy() + centroid = np.mean(pts[:, :3], axis=0) + points_centered = pts[:, :3] - centroid + + dist_from_plane = np.dot(points_centered, rand_xyz) + + mask = dist_from_plane > np.percentile(dist_from_plane, (1.0 - p_keep) * 100) + idx_x = torch.Tensor(np.nonzero(mask)) + + return torch.Tensor(pts[mask, :3]), idx_x + +def knn_idx(pts, k): + kdt = cKDTree(pts) + _, idx = kdt.query(pts, k=k+1) + return idx[:, 1:] + +def get_rri(pts, k): + # pts: N x 3, original points + # q: N x K x 3, nearest neighbors + q = pts[knn_idx(pts, k)] + p = np.repeat(pts[:, None], k, axis=1) + # rp, rq: N x K x 1, norms + rp = np.linalg.norm(p, axis=-1, keepdims=True) + rq = np.linalg.norm(q, axis=-1, keepdims=True) + pn = p / rp + qn = q / rq + dot = np.sum(pn * qn, -1, keepdims=True) + # theta: N x K x 1, angles + theta = np.arccos(np.clip(dot, -1, 1)) + T_q = q - dot * p + sin_psi = np.sum(np.cross(T_q[:, None], T_q[:, :, None]) * pn[:, None], -1) + cos_psi = np.sum(T_q[:, None] * T_q[:, :, None], -1) + psi = np.arctan2(sin_psi, cos_psi) % (2*np.pi) + idx = np.argpartition(psi, 1)[:, :, 1:2] + # phi: N x K x 1, projection angles + phi = np.take_along_axis(psi, idx, axis=-1) + feat = np.concatenate([rp, rq, theta, phi], axis=-1) + return feat.reshape(-1, k * 4) + +def get_rri_cuda(pts, k, npts_per_block=1): + try: + import pycuda.autoinit + from pycuda import gpuarray + from pycuda.compiler import SourceModule + except Exception as e: + print("Error raised in pycuda modules! pycuda only works with GPU, ", e) + raise + + mod_rri = SourceModule(open('rri.cu').read() % (k, npts_per_block)) + rri_cuda = mod_rri.get_function('get_rri_feature') + + N = len(pts) + pts_gpu = gpuarray.to_gpu(pts.astype(np.float32).ravel()) + k_idx = knn_idx(pts, k) + k_idx_gpu = gpuarray.to_gpu(k_idx.astype(np.int32).ravel()) + feat_gpu = gpuarray.GPUArray((N * k * 4,), np.float32) + + rri_cuda(pts_gpu, np.int32(N), k_idx_gpu, feat_gpu, + grid=(((N-1) // npts_per_block)+1, 1), + block=(npts_per_block, k, 1)) + + feat = feat_gpu.get().reshape(N, k * 4).astype(np.float32) + return feat + + +class UnknownDataTypeError(Exception): + def __init__(self, *args): + if args: self.message = args[0] + else: self.message = 'Datatype not understood for dataset.' + + def __str__(self): + return self.message + + +class ModelNet40Data(Dataset): + def __init__( + self, + train=True, + num_points=1024, + download=True, + randomize_data=False, + use_normals=False + ): + super(ModelNet40Data, self).__init__() + if download: download_modelnet40() + self.data, self.labels = load_data(train, use_normals) + if not train: self.shapes = self.read_classes_ModelNet40() + self.num_points = num_points + self.randomize_data = randomize_data + + def __getitem__(self, idx): + if self.randomize_data: current_points = self.randomize(idx) + else: current_points = self.data[idx].copy() + + current_points = torch.from_numpy(current_points[:self.num_points, :]).float() + label = torch.from_numpy(self.labels[idx]).type(torch.LongTensor) + + return current_points, label + + def __len__(self): + return self.data.shape[0] + + def randomize(self, idx): + pt_idxs = np.arange(0, self.num_points) + np.random.shuffle(pt_idxs) + return self.data[idx, pt_idxs].copy() + + def get_shape(self, label): + return self.shapes[label] + + def read_classes_ModelNet40(self): + BASE_DIR = os.path.dirname(os.path.abspath(__file__)) + DATA_DIR = os.path.join(BASE_DIR, os.pardir, 'data') + file = open(os.path.join(DATA_DIR, 'modelnet40_ply_hdf5_2048', 'shape_names.txt'), 'r') + shape_names = file.read() + shape_names = np.array(shape_names.split('\n')[:-1]) + return shape_names + + +class ClassificationData(Dataset): + def __init__(self, data_class=ModelNet40Data()): + super(ClassificationData, self).__init__() + self.set_class(data_class) + + def __len__(self): + return len(self.data_class) + + def set_class(self, data_class): + self.data_class = data_class + + def get_shape(self, label): + try: + return self.data_class.get_shape(label) + except: + return -1 + + def __getitem__(self, index): + return self.data_class[index] + + +class RegistrationData(Dataset): + def __init__(self, algorithm, data_class=ModelNet40Data(), partial_source=False, partial_template=False, noise=False, additional_params={}): + super(RegistrationData, self).__init__() + available_algorithms = ['PCRNet', 'PointNetLK', 'DCP', 'PRNet', 'iPCRNet', 'RPMNet', 'DeepGMR'] + if algorithm in available_algorithms: self.algorithm = algorithm + else: raise Exception("Algorithm not available for registration.") + + self.set_class(data_class) + self.partial_template = partial_template + self.partial_source = partial_source + self.noise = noise + self.additional_params = additional_params + self.use_rri = False + + if self.algorithm == 'PCRNet' or self.algorithm == 'iPCRNet': + from .. ops.transform_functions import PCRNetTransform + self.transforms = PCRNetTransform(len(data_class), angle_range=45, translation_range=1) + if self.algorithm == 'PointNetLK': + from .. ops.transform_functions import PNLKTransform + self.transforms = PNLKTransform(0.8, True) + if self.algorithm == 'RPMNet': + from .. ops.transform_functions import RPMNetTransform + self.transforms = RPMNetTransform(0.8, True) + if self.algorithm == 'DCP' or self.algorithm == 'PRNet': + from .. ops.transform_functions import DCPTransform + self.transforms = DCPTransform(angle_range=45, translation_range=1) + if self.algorithm == 'DeepGMR': + self.get_rri = get_rri_cuda if torch.cuda.is_available() else get_rri + from .. ops.transform_functions import DeepGMRTransform + self.transforms = DeepGMRTransform(angle_range=90, translation_range=1) + if 'nearest_neighbors' in self.additional_params.keys() and self.additional_params['nearest_neighbors'] > 0: + self.use_rri = True + self.nearest_neighbors = self.additional_params['nearest_neighbors'] + + def __len__(self): + return len(self.data_class) + + def set_class(self, data_class): + self.data_class = data_class + + def __getitem__(self, index): + template, label = self.data_class[index] + self.transforms.index = index # for fixed transformations in PCRNet. + source = self.transforms(template) + + # Check for Partial Data. + if self.additional_params.get('partial_point_cloud_method', None) == 'planar_crop': + source, gt_idx_source = planar_crop(source) + template, gt_idx_template = planar_crop(template) + intersect_mask, intersect_x, intersect_y = np.intersect1d(gt_idx_source, gt_idx_template, return_indices=True) + + self.template_mask = torch.zeros(template.shape[0]) + self.source_mask = torch.zeros(source.shape[0]) + self.template_mask[intersect_y] = 1 + self.source_mask[intersect_x] = 1 + else: + if self.partial_source: source, self.source_mask = farthest_subsample_points(source) + if self.partial_template: template, self.template_mask = farthest_subsample_points(template) + + + + # Check for Noise in Source Data. + if self.noise: source = jitter_pointcloud(source) + + if self.use_rri: + template, source = template.numpy(), source.numpy() + template = np.concatenate([template, self.get_rri(template - template.mean(axis=0), self.nearest_neighbors)], axis=1) + source = np.concatenate([source, self.get_rri(source - source.mean(axis=0), self.nearest_neighbors)], axis=1) + template, source = torch.tensor(template).float(), torch.tensor(source).float() + + igt = self.transforms.igt + + if self.additional_params.get('use_masknet', False): + if self.partial_source and self.partial_template: + return template, source, igt, self.template_mask, self.source_mask + elif self.partial_source: + return template, source, igt, self.source_mask + elif self.partial_template: + return template, source, igt, self.template_mask + else: + return template, source, igt + + +class SegmentationData(Dataset): + def __init__(self): + super(SegmentationData, self).__init__() + + def __len__(self): + pass + + def __getitem__(self, index): + pass + + +class FlowData(Dataset): + def __init__(self): + super(FlowData, self).__init__() + self.pc1, self.pc2, self.flow = self.read_data() + + def __len__(self): + if isinstance(self.pc1, np.ndarray): + return self.pc1.shape[0] + elif isinstance(self.pc1, list): + return len(self.pc1) + else: + raise UnknownDataTypeError + + def read_data(self): + pass + + def __getitem__(self, index): + return self.pc1[index], self.pc2[index], self.flow[index] + + +class SceneflowDataset(Dataset): + def __init__(self, npoints=1024, root='', partition='train'): + if root == '': + BASE_DIR = os.path.dirname(os.path.abspath(__file__)) + DATA_DIR = os.path.join(BASE_DIR, os.pardir, 'data') + root = os.path.join(DATA_DIR, 'data_processed_maxcut_35_20k_2k_8192') + if not os.path.exists(root): + print("To download dataset, click here: https://drive.google.com/file/d/1CMaxdt-Tg1Wct8v8eGNwuT7qRSIyJPY-/view") + exit() + else: + print("SceneflowDataset Found Successfully!") + + self.npoints = npoints + self.partition = partition + self.root = root + if self.partition=='train': + self.datapath = glob.glob(os.path.join(self.root, 'TRAIN*.npz')) + else: + self.datapath = glob.glob(os.path.join(self.root, 'TEST*.npz')) + self.cache = {} + self.cache_size = 30000 + + ###### deal with one bad datapoint with nan value + self.datapath = [d for d in self.datapath if 'TRAIN_C_0140_left_0006-0' not in d] + ###### + print(self.partition, ': ',len(self.datapath)) + + def __getitem__(self, index): + if index in self.cache: + pos1, pos2, color1, color2, flow, mask1 = self.cache[index] + else: + fn = self.datapath[index] + with open(fn, 'rb') as fp: + data = np.load(fp) + pos1 = data['points1'].astype('float32') + pos2 = data['points2'].astype('float32') + color1 = data['color1'].astype('float32') + color2 = data['color2'].astype('float32') + flow = data['flow'].astype('float32') + mask1 = data['valid_mask1'] + + if len(self.cache) < self.cache_size: + self.cache[index] = (pos1, pos2, color1, color2, flow, mask1) + + if self.partition == 'train': + n1 = pos1.shape[0] + sample_idx1 = np.random.choice(n1, self.npoints, replace=False) + n2 = pos2.shape[0] + sample_idx2 = np.random.choice(n2, self.npoints, replace=False) + + pos1 = pos1[sample_idx1, :] + pos2 = pos2[sample_idx2, :] + color1 = color1[sample_idx1, :] + color2 = color2[sample_idx2, :] + flow = flow[sample_idx1, :] + mask1 = mask1[sample_idx1] + else: + pos1 = pos1[:self.npoints, :] + pos2 = pos2[:self.npoints, :] + color1 = color1[:self.npoints, :] + color2 = color2[:self.npoints, :] + flow = flow[:self.npoints, :] + mask1 = mask1[:self.npoints] + + pos1_center = np.mean(pos1, 0) + pos1 -= pos1_center + pos2 -= pos1_center + + return pos1, pos2, color1, color2, flow, mask1 + + def __len__(self): + return len(self.datapath) + + +if __name__ == '__main__': + class Data(): + def __init__(self): + super(Data, self).__init__() + self.data, self.label = self.read_data() + + def read_data(self): + return [4,5,6], [4,5,6] + + def __len__(self): + return len(self.data) + + def __getitem__(self, idx): + return self.data[idx], self.label[idx] + + cd = RegistrationData('abc') + import ipdb; ipdb.set_trace() diff --git a/thirdparty/learning3d/data_utils/user_data.py b/thirdparty/learning3d/data_utils/user_data.py new file mode 100644 index 0000000000000000000000000000000000000000..1bac84b36d2b9d755ea3ff3908f22414e53c32b7 --- /dev/null +++ b/thirdparty/learning3d/data_utils/user_data.py @@ -0,0 +1,119 @@ +import os +import numpy as np +import torch + +class ClassificationData: + def __init__(self, data_dict): + self.data_dict = data_dict + self.pcs = self.find_attribute('pcs') + self.labels = self.find_attribute('labels') + self.check_data() + + def find_attribute(self, attribute): + try: + attribute_data = self.data_dict[attribute] + except: + print("Given data directory has no key attribute \"{}\"".format(attribute)) + return attribute_data + + def check_data(self): + assert 1 < len(self.pcs.shape) < 4, "Error in dimension of point clouds! Given data dimension: {}".format(self.pcs.shape) + assert 0 < len(self.labels.shape) < 3, "Error in dimension of labels! Given data dimension: {}".format(self.labels.shape) + + if len(self.pcs.shape)==2: self.pcs = self.pcs.reshape(1, -1, 3) + if len(self.labels.shape) == 1: self.labels = self.labels.reshape(1, -1) + + assert self.pcs.shape[0] == self.labels.shape[0], "Inconsistency in the number of point clouds and number of ground truth labels!" + + + def __len__(self): + return self.pcs.shape[0] + + def __getitem__(self, index): + return torch.tensor(self.pcs[index]).float(), torch.from_numpy(self.labels[idx]).type(torch.LongTensor) + + +class RegistrationData: + def __init__(self, data_dict): + self.data_dict = data_dict + self.template = self.find_attribute('template') + self.source = self.find_attribute('source') + self.transformation = self.find_attribute('transformation') + self.check_data() + + def find_attribute(self, attribute): + try: + attribute_data = self.data[attribute] + except: + print("Given data directory has no key attribute \"{}\"".format(attribute)) + return attribute_data + + def check_data(self): + assert 1 < len(self.template.shape) < 4, "Error in dimension of point clouds! Given data dimension: {}".format(self.template.shape) + assert 1 < len(self.source.shape) < 4, "Error in dimension of point clouds! Given data dimension: {}".format(self.source.shape) + assert 1 < len(self.transformation.shape) < 4, "Error in dimension of transformations! Given data dimension: {}".format(self.transformation.shape) + + if len(self.template.shape)==2: self.template = self.template.reshape(1, -1, 3) + if len(self.source.shape)==2: self.source = self.source.reshape(1, -1, 3) + if len(self.transformation.shape) == 2: self.transformation = self.transformation.reshape(1, 4, 4) + + assert self.template.shape[0] == self.source.shape[0], "Inconsistency in the number of template and source point clouds!" + assert self.source.shape[0] == self.transformation.shape[0], "Inconsistency in the number of transformation and source point clouds!" + + def __len__(self): + return self.template.shape[0] + + def __getitem__(self, index): + return torch.tensor(self.template[index]).float(), torch.tensor(self.source[index]).float(), torch.tensor(self.transformation[index]).float() + + +class FlowData: + def __init__(self, data_dict): + self.data_dict = data_dict + self.frame1 = self.find_attribute('frame1') + self.frame2 = self.find_attribute('frame2') + self.flow = self.find_attribute('flow') + self.check_data() + + def find_attribute(self, attribute): + try: + attribute_data = self.data[attribute] + except: + print("Given data directory has no key attribute \"{}\"".format(attribute)) + return attribute_data + + def check_data(self): + assert 1 < len(self.frame1.shape) < 4, "Error in dimension of point clouds! Given data dimension: {}".format(self.frame1.shape) + assert 1 < len(self.frame2.shape) < 4, "Error in dimension of point clouds! Given data dimension: {}".format(self.frame2.shape) + assert 1 < len(self.flow.shape) < 4, "Error in dimension of flow! Given data dimension: {}".format(self.flow.shape) + + if len(self.frame1.shape)==2: self.frame1 = self.frame1.reshape(1, -1, 3) + if len(self.frame2.shape)==2: self.frame2 = self.frame2.reshape(1, -1, 3) + if len(self.flow.shape) == 2: self.flow = self.flow.reshape(1, -1, 3) + + assert self.frame1.shape[0] == self.frame2.shape[0], "Inconsistency in the number of frame1 and frame2 point clouds!" + assert self.frame2.shape[0] == self.flow.shape[0], "Inconsistency in the number of flow and frame2 point clouds!" + + def __len__(self): + return self.frame1.shape[0] + + def __getitem__(self, index): + return torch.tensor(self.frame1[index]).float(), torch.tensor(self.frame2[index]).float(), torch.tensor(self.flow[index]).float() + + +class UserData: + def __init__(self, application, data_dict): + self.application = application + + if self.application == 'classification': + self.data_class = ClassificationData(data_dict) + elif self.application == 'registration': + self.data_class = RegistrationData(data_dict) + elif self.application == 'flow_estimation': + self.data_class = FlowData(data_dict) + + def __len__(self): + return len(self.data_class) + + def __getitem__(self, index): + return self.data_class[index] diff --git a/thirdparty/learning3d/examples/test_curvenet.py b/thirdparty/learning3d/examples/test_curvenet.py new file mode 100644 index 0000000000000000000000000000000000000000..155a05098cfb9630bfdcc4130af3a49e54de8496 --- /dev/null +++ b/thirdparty/learning3d/examples/test_curvenet.py @@ -0,0 +1,118 @@ +import open3d as o3d +import argparse +import os +import sys +import logging +import numpy +import numpy as np +import torch +import torch.utils.data +import torchvision +from torch.utils.data import DataLoader +from tensorboardX import SummaryWriter +from tqdm import tqdm + +# Only if the files are in example folder. +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +if BASE_DIR[-8:] == 'examples': + sys.path.append(os.path.join(BASE_DIR, os.pardir)) + os.chdir(os.path.join(BASE_DIR, os.pardir)) + +from learning3d.models import CurveNet +from learning3d.data_utils import ClassificationData, ModelNet40Data + +def display_open3d(template): + template_ = o3d.geometry.PointCloud() + template_.points = o3d.utility.Vector3dVector(template) + # template_.paint_uniform_color([1, 0, 0]) + o3d.visualization.draw_geometries([template_]) + +def test_one_epoch(device, model, test_loader, testset): + model.eval() + test_loss = 0.0 + pred = 0.0 + count = 0 + for i, data in enumerate(tqdm(test_loader)): + points, target = data + target = target[:,0] + + points = points.to(device) + target = target.to(device) + + output = model(points) + loss_val = torch.nn.functional.nll_loss( + torch.nn.functional.log_softmax(output, dim=1), target, size_average=False) + print("Ground Truth Label: ", testset.get_shape(target[0].item())) + print("Predicted Label: ", testset.get_shape(torch.argmax(output[0]).item())) + display_open3d(points.detach().cpu().numpy()[0]) + + test_loss += loss_val.item() + count += output.size(0) + + _, pred1 = output.max(dim=1) + ag = (pred1 == target) + am = ag.sum() + pred += am.item() + + test_loss = float(test_loss)/count + accuracy = float(pred)/count + return test_loss, accuracy + +def test(args, model, test_loader, testset): + test_loss, test_accuracy = test_one_epoch(args.device, model, test_loader, testset) + print("Accuracy: ", test_accuracy*100) + +def options(): + parser = argparse.ArgumentParser(description='Point Cloud Registration') + parser.add_argument('--dataset_path', type=str, default='ModelNet40', + metavar='PATH', help='path to the input dataset') # like '/path/to/ModelNet40' + parser.add_argument('--eval', type=bool, default=False, help='Train or Evaluate the network.') + + # settings for input data + parser.add_argument('--dataset_type', default='modelnet', choices=['modelnet', 'shapenet2'], + metavar='DATASET', help='dataset type (default: modelnet)') + parser.add_argument('--num_points', default=1024, type=int, + metavar='N', help='points in point-cloud (default: 1024)') + + # settings for CurveNet + parser.add_argument('-j', '--workers', default=4, type=int, + metavar='N', help='number of data loading workers (default: 4)') + parser.add_argument('-b', '--batch_size', default=32, type=int, + metavar='N', help='mini-batch size (default: 32)') + parser.add_argument('--num_classes', default=40, type=int, + metavar='K', help='number of classes to be predicted') + + # settings for on training + parser.add_argument('--pretrained', default='learning3d/pretrained/exp_curvenet/models/model.t7', type=str, + metavar='PATH', help='path to pretrained model file (default: null (no-use))') + parser.add_argument('--device', default='cuda:0', type=str, + metavar='DEVICE', help='use CUDA if available') + + args = parser.parse_args() + return args + +def main(): + args = options() + args.dataset_path = os.path.join(os.getcwd(), os.pardir, os.pardir, 'ModelNet40', 'ModelNet40') + + testset = ClassificationData(ModelNet40Data(train=False)) + test_loader = DataLoader(testset, batch_size=args.batch_size, shuffle=False, drop_last=False, num_workers=args.workers) + + if not torch.cuda.is_available(): + args.device = 'cpu' + args.device = torch.device(args.device) + + # Create PointNet Model. + model = CurveNet(num_classes=args.num_classes, k=20) + + if args.pretrained: + assert os.path.isfile(args.pretrained) + weights = torch.load(args.pretrained, map_location='cpu') + weights = {k[7:]: v for k, v in weights.items()} + model.load_state_dict(weights) + model.to(args.device) + + test(args, model, test_loader, testset) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/thirdparty/learning3d/examples/test_dcp.py b/thirdparty/learning3d/examples/test_dcp.py new file mode 100644 index 0000000000000000000000000000000000000000..6a8adf8d41b8eff414d2a33d69cf9732ce970a10 --- /dev/null +++ b/thirdparty/learning3d/examples/test_dcp.py @@ -0,0 +1,139 @@ +import open3d as o3d +import argparse +import os +import sys +import logging +import numpy +import numpy as np +import torch +import torch.utils.data +import torchvision +from torch.utils.data import DataLoader +from tensorboardX import SummaryWriter +from tqdm import tqdm + +# Only if the files are in example folder. +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +if BASE_DIR[-8:] == 'examples': + sys.path.append(os.path.join(BASE_DIR, os.pardir)) + os.chdir(os.path.join(BASE_DIR, os.pardir)) + +from learning3d.models import DGCNN, DCP +from learning3d.data_utils import RegistrationData, ModelNet40Data + +def get_transformations(igt): + R_ba = igt[:, 0:3, 0:3] # Ps = R_ba * Pt + translation_ba = igt[:, 0:3, 3].unsqueeze(2) # Ps = Pt + t_ba + R_ab = R_ba.permute(0, 2, 1) # Pt = R_ab * Ps + translation_ab = -torch.bmm(R_ab, translation_ba) # Pt = Ps + t_ab + return R_ab, translation_ab, R_ba, translation_ba + +def display_open3d(template, source, transformed_source): + template_ = o3d.geometry.PointCloud() + source_ = o3d.geometry.PointCloud() + transformed_source_ = o3d.geometry.PointCloud() + template_.points = o3d.utility.Vector3dVector(template) + source_.points = o3d.utility.Vector3dVector(source + np.array([0,0,0])) + transformed_source_.points = o3d.utility.Vector3dVector(transformed_source) + template_.paint_uniform_color([1, 0, 0]) + source_.paint_uniform_color([0, 1, 0]) + transformed_source_.paint_uniform_color([0, 0, 1]) + o3d.visualization.draw_geometries([template_, source_, transformed_source_]) + +def test_one_epoch(device, model, test_loader): + model.eval() + test_loss = 0.0 + pred = 0.0 + count = 0 + for i, data in enumerate(tqdm(test_loader)): + template, source, igt = data + transformations = get_transformations(igt) + transformations = [t.to(device) for t in transformations] + R_ab, translation_ab, R_ba, translation_ba = transformations + + template = template.to(device) + source = source.to(device) + igt = igt.to(device) + + output = model(template, source) + display_open3d(template.detach().cpu().numpy()[0], source.detach().cpu().numpy()[0], output['transformed_source'].detach().cpu().numpy()[0]) + + identity = torch.eye(3).cuda().unsqueeze(0).repeat(template.shape[0], 1, 1) + loss_val = torch.nn.functional.mse_loss(torch.matmul(output['est_R'].transpose(2, 1), R_ab), identity) \ + + torch.nn.functional.mse_loss(output['est_t'], translation_ab[:,:,0]) + + cycle_loss = torch.nn.functional.mse_loss(torch.matmul(output['est_R_'].transpose(2, 1), R_ba), identity) \ + + torch.nn.functional.mse_loss(output['est_t_'], translation_ba[:,:,0]) + loss_val = loss_val + cycle_loss * 0.1 + + test_loss += loss_val.item() + count += 1 + + test_loss = float(test_loss)/count + return test_loss + +def test(args, model, test_loader): + test_loss, test_accuracy = test_one_epoch(args.device, model, test_loader) + +def options(): + parser = argparse.ArgumentParser(description='Point Cloud Registration') + parser.add_argument('--exp_name', type=str, default='exp_ipcrnet', metavar='N', + help='Name of the experiment') + parser.add_argument('--dataset_path', type=str, default='ModelNet40', + metavar='PATH', help='path to the input dataset') # like '/path/to/ModelNet40' + parser.add_argument('--eval', type=bool, default=False, help='Train or Evaluate the network.') + + # settings for input data + parser.add_argument('--dataset_type', default='modelnet', choices=['modelnet', 'shapenet2'], + metavar='DATASET', help='dataset type (default: modelnet)') + parser.add_argument('--num_points', default=1024, type=int, + metavar='N', help='points in point-cloud (default: 1024)') + + # settings for PointNet + parser.add_argument('--pointnet', default='tune', type=str, choices=['fixed', 'tune'], + help='train pointnet (default: tune)') + parser.add_argument('--emb_dims', default=512, type=int, + metavar='K', help='dim. of the feature vector (default: 1024)') + parser.add_argument('--symfn', default='max', choices=['max', 'avg'], + help='symmetric function (default: max)') + + # settings for on training + parser.add_argument('-j', '--workers', default=4, type=int, + metavar='N', help='number of data loading workers (default: 4)') + parser.add_argument('-b', '--batch_size', default=2, type=int, + metavar='N', help='mini-batch size (default: 32)') + parser.add_argument('--pretrained', default='learning3d/pretrained/exp_dcp/models/best_model.t7', type=str, + metavar='PATH', help='path to pretrained model file (default: null (no-use))') + parser.add_argument('--device', default='cuda:0', type=str, + metavar='DEVICE', help='use CUDA if available') + + args = parser.parse_args() + return args + +def main(): + args = options() + torch.backends.cudnn.deterministic = True + + trainset = RegistrationData('DCP', ModelNet40Data(train=True)) + testset = RegistrationData('DCP', ModelNet40Data(train=False)) + train_loader = DataLoader(trainset, batch_size=args.batch_size, shuffle=True, drop_last=True, num_workers=args.workers) + test_loader = DataLoader(testset, batch_size=args.batch_size, shuffle=False, drop_last=False, num_workers=args.workers) + + if not torch.cuda.is_available(): + args.device = 'cpu' + args.device = torch.device(args.device) + + # Create PointNet Model. + dgcnn = DGCNN(emb_dims=args.emb_dims) + model = DCP(feature_model=dgcnn, cycle=True) + model = model.to(args.device) + + if args.pretrained: + assert os.path.isfile(args.pretrained) + model.load_state_dict(torch.load(args.pretrained, map_location='cpu'), strict=False) + model.to(args.device) + + test(args, model, test_loader) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/thirdparty/learning3d/examples/test_deepgmr.py b/thirdparty/learning3d/examples/test_deepgmr.py new file mode 100644 index 0000000000000000000000000000000000000000..ebd9da7e2d2596a697282a24d115070e44274d2f --- /dev/null +++ b/thirdparty/learning3d/examples/test_deepgmr.py @@ -0,0 +1,144 @@ +import open3d as o3d +import argparse +import os +import sys +import logging +import numpy +import numpy as np +import torch +import torch.utils.data +import torchvision +from torch.utils.data import DataLoader +from tensorboardX import SummaryWriter +from tqdm import tqdm + +# Only if the files are in example folder. +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +if BASE_DIR[-8:] == 'examples': + sys.path.append(os.path.join(BASE_DIR, os.pardir)) + os.chdir(os.path.join(BASE_DIR, os.pardir)) + +from learning3d.models import DeepGMR +from learning3d.data_utils import RegistrationData, ModelNet40Data + +def display_open3d(template, source, transformed_source): + template_ = o3d.geometry.PointCloud() + source_ = o3d.geometry.PointCloud() + transformed_source_ = o3d.geometry.PointCloud() + template_.points = o3d.utility.Vector3dVector(template) + source_.points = o3d.utility.Vector3dVector(source + np.array([0,0,0])) + transformed_source_.points = o3d.utility.Vector3dVector(transformed_source) + template_.paint_uniform_color([1, 0, 0]) + source_.paint_uniform_color([0, 1, 0]) + transformed_source_.paint_uniform_color([0, 0, 1]) + o3d.visualization.draw_geometries([template_, source_, transformed_source_]) + +def rotation_error(R, R_gt): + cos_theta = (torch.einsum('bij,bij->b', R, R_gt) - 1) / 2 + cos_theta = torch.clamp(cos_theta, -1, 1) + return torch.acos(cos_theta) * 180 / math.pi + +def translation_error(t, t_gt): + return torch.norm(t - t_gt, dim=1) + +def rmse(pts, T, T_gt): + pts_pred = pts @ T[:, :3, :3].transpose(1, 2) + T[:, :3, 3].unsqueeze(1) + pts_gt = pts @ T_gt[:, :3, :3].transpose(1, 2) + T_gt[:, :3, 3].unsqueeze(1) + return torch.norm(pts_pred - pts_gt, dim=2).mean(dim=1) + +def test_one_epoch(device, model, test_loader): + model.eval() + test_loss = 0.0 + pred = 0.0 + count = 0 + rotation_errors, translation_errors, rmses = [], [], [] + + for i, data in enumerate(tqdm(test_loader)): + template, source, igt = data + + template = template.to(device) + source = source.to(device) + igt = igt.to(device) + + output = model(template, source) + display_open3d(template.detach().cpu().numpy()[0, :, :3], source.detach().cpu().numpy()[0, :, :3], output['transformed_source'].detach().cpu().numpy()[0]) + + eye = torch.eye(4).expand_as(igt).to(igt.device) + mse1 = F.mse_loss(output['est_T_inverse'] @ torch.inverse(igt), eye) + mse2 = F.mse_loss(output['est_T'] @ igt, eye) + loss = mse1 + mse2 + + r_err = rotation_error(est_T_inverse[:, :3, :3], igt[:, :3, :3]) + t_err = translation_error(est_T_inverse[:, :3, 3], igt[:, :3, 3]) + rmse_val = rmse(template[:, :100], est_T_inverse, igt) + rotation_errors.append(r_err) + translation_errors.append(t_err) + rmses.append(rmse_val) + + test_loss += loss_val.item() + count += 1 + + test_loss = float(test_loss)/count + print("Mean rotation error: {}, Mean translation error: {} and Mean RMSE: {}".format(np.mean(rotation_errors), np.mean(translation_errors), np.mean(rmses))) + return test_loss + +def test(args, model, test_loader): + test_loss = test_one_epoch(args.device, model, test_loader) + +def options(): + parser = argparse.ArgumentParser(description='Point Cloud Registration') + parser.add_argument('--exp_name', type=str, default='exp_deepgmr', metavar='N', + help='Name of the experiment') + parser.add_argument('--dataset_path', type=str, default='ModelNet40', + metavar='PATH', help='path to the input dataset') # like '/path/to/ModelNet40' + parser.add_argument('--eval', type=bool, default=False, help='Train or Evaluate the network.') + + # settings for input data + parser.add_argument('--dataset_type', default='modelnet', choices=['modelnet', 'shapenet2'], + metavar='DATASET', help='dataset type (default: modelnet)') + parser.add_argument('--num_points', default=1024, type=int, + metavar='N', help='points in point-cloud (default: 1024)') + + parser.add_argument('--nearest_neighbors', default=20, type=int, + metavar='K', help='No of nearest neighbors to be estimated.') + parser.add_argument('--use_rri', default=True, type=bool, + help='Find nearest neighbors to estimate features from PointNet.') + + # settings for on training + parser.add_argument('-j', '--workers', default=4, type=int, + metavar='N', help='number of data loading workers (default: 4)') + parser.add_argument('-b', '--batch_size', default=2, type=int, + metavar='N', help='mini-batch size (default: 32)') + parser.add_argument('--pretrained', default='learning3d/pretrained/exp_deepgmr/models/best_model.pth', type=str, + metavar='PATH', help='path to pretrained model file (default: null (no-use))') + parser.add_argument('--device', default='cuda:0', type=str, + metavar='DEVICE', help='use CUDA if available') + + args = parser.parse_args() + return args + +def main(): + args = options() + torch.backends.cudnn.deterministic = True + + trainset = RegistrationData('DeepGMR', ModelNet40Data(train=True)) + testset = RegistrationData('DeepGMR', ModelNet40Data(train=False)) + train_loader = DataLoader(trainset, batch_size=args.batch_size, shuffle=True, drop_last=True, num_workers=args.workers) + test_loader = DataLoader(testset, batch_size=args.batch_size, shuffle=False, drop_last=False, num_workers=args.workers) + + if not torch.cuda.is_available(): + args.device = 'cpu' + args.device = torch.device(args.device) + + model = DeepGMR(use_rri=args.use_rri, nearest_neighbors=args.nearest_neighbors) + model = model.to(args.device) + + if args.pretrained: + assert os.path.isfile(args.pretrained) + model.load_state_dict(torch.load(args.pretrained, map_location='cpu'), strict=False) + model.to(args.device) + + test(args, model, test_loader) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/thirdparty/learning3d/examples/test_flownet.py b/thirdparty/learning3d/examples/test_flownet.py new file mode 100644 index 0000000000000000000000000000000000000000..35bd538b0ba52b62d9b15d760e036c3956deefd4 --- /dev/null +++ b/thirdparty/learning3d/examples/test_flownet.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + + +import open3d as o3d +import os +import gc +import argparse +import torch +import torch.nn as nn +import torch.nn.functional as F +import torch.optim as optim +from torch.optim.lr_scheduler import MultiStepLR +from learning3d.models import FlowNet3D +from learning3d.data_utils import SceneflowDataset +import numpy as np +from torch.utils.data import DataLoader +from tensorboardX import SummaryWriter +from tqdm import tqdm + +def display_open3d(template, source, transformed_source): + template_ = o3d.geometry.PointCloud() + source_ = o3d.geometry.PointCloud() + transformed_source_ = o3d.geometry.PointCloud() + template_.points = o3d.utility.Vector3dVector(template) + source_.points = o3d.utility.Vector3dVector(source + np.array([0,0.5,0.5])) + transformed_source_.points = o3d.utility.Vector3dVector(transformed_source) + template_.paint_uniform_color([1, 0, 0]) + source_.paint_uniform_color([0, 1, 0]) + transformed_source_.paint_uniform_color([0, 0, 1]) + o3d.visualization.draw_geometries([template_, source_, transformed_source_]) + +def test_one_epoch(args, net, test_loader): + net.eval() + + total_loss = 0 + num_examples = 0 + for i, data in enumerate(tqdm(test_loader)): + data = [d.to(args.device) for d in data] + pc1, pc2, color1, color2, flow, mask1 = data + pc1 = pc1.transpose(2,1).contiguous() + pc2 = pc2.transpose(2,1).contiguous() + color1 = color1.transpose(2,1).contiguous() + color2 = color2.transpose(2,1).contiguous() + flow = flow + mask1 = mask1.float() + + batch_size = pc1.size(0) + num_examples += batch_size + flow_pred = net(pc1, pc2, color1, color2).permute(0,2,1) + loss_1 = torch.mean(mask1 * torch.sum((flow_pred - flow) * (flow_pred - flow), -1) / 2.0) + + pc1, pc2 = pc1.permute(0,2,1), pc2.permute(0,2,1) + pc1_ = pc1 - flow_pred + print("Loss: ", loss_1) + display_open3d(pc1.detach().cpu().numpy()[0], pc2.detach().cpu().numpy()[0], pc1_.detach().cpu().numpy()[0]) + total_loss += loss_1.item() * batch_size + + return total_loss * 1.0 / num_examples + + +def test(args, net, test_loader): + test_loss = test_one_epoch(args, net, test_loader) + +def main(): + parser = argparse.ArgumentParser(description='Point Cloud Registration') + parser.add_argument('--model', type=str, default='flownet', metavar='N', + choices=['flownet'], help='Model to use, [flownet]') + parser.add_argument('--emb_dims', type=int, default=512, metavar='N', + help='Dimension of embeddings') + parser.add_argument('--num_points', type=int, default=2048, + help='Point Number [default: 2048]') + parser.add_argument('--test_batch_size', type=int, default=1, metavar='batch_size', + help='Size of batch)') + + parser.add_argument('--gaussian_noise', type=bool, default=False, metavar='N', + help='Wheter to add gaussian noise') + parser.add_argument('--unseen', type=bool, default=False, metavar='N', + help='Whether to test on unseen category') + parser.add_argument('--dataset', type=str, default='SceneflowDataset', + choices=['SceneflowDataset'], metavar='N', + help='dataset to use') + parser.add_argument('--dataset_path', type=str, default='data_processed_maxcut_35_20k_2k_8192', metavar='N', + help='dataset to use') + parser.add_argument('--pretrained', type=str, default='learning3d/pretrained/exp_flownet/models/model.best.t7', metavar='N', + help='Pretrained model path') + parser.add_argument('--device', default='cuda:0', type=str, + metavar='DEVICE', help='use CUDA if available') + + args = parser.parse_args() + if not torch.cuda.is_available(): + args.device = torch.device('cpu') + else: + args.device = torch.device('cuda') + + if args.dataset == 'SceneflowDataset': + test_loader = DataLoader( + SceneflowDataset(npoints=args.num_points, partition='test'), + batch_size=args.test_batch_size, shuffle=False, drop_last=False) + else: + raise Exception("not implemented") + + net = FlowNet3D() + assert os.path.exists(args.pretrained), "Pretrained Model Doesn't Exists!" + net.load_state_dict(torch.load(args.pretrained, map_location='cpu')) + net = net.to(args.device) + + test(args, net, test_loader) + print('FINISH') + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/thirdparty/learning3d/examples/test_masknet.py b/thirdparty/learning3d/examples/test_masknet.py new file mode 100644 index 0000000000000000000000000000000000000000..416587083492f1b21ffe6306a66fe161fd58d576 --- /dev/null +++ b/thirdparty/learning3d/examples/test_masknet.py @@ -0,0 +1,159 @@ +import open3d as o3d +import argparse +import os +import sys +import logging +import numpy +import numpy as np +import torch +import torch.utils.data +import torchvision +from torch.utils.data import DataLoader +from tensorboardX import SummaryWriter +from tqdm import tqdm + +# Only if the files are in example folder. +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +if BASE_DIR[-8:] == 'examples': + sys.path.append(os.path.join(BASE_DIR, os.pardir)) + os.chdir(os.path.join(BASE_DIR, os.pardir)) + +from learning3d.models import MaskNet +from learning3d.data_utils import RegistrationData, ModelNet40Data + +def pc2open3d(data): + if torch.is_tensor(data): data = data.detach().cpu().numpy() + if len(data.shape) == 2: + pc = o3d.geometry.PointCloud() + pc.points = o3d.utility.Vector3dVector(data) + return pc + else: + print("Error in the shape of data given to Open3D!, Shape is ", data.shape) + +def display_results(template, source, masked_template): + template = pc2open3d(template) + source = pc2open3d(source) + masked_template = pc2open3d(masked_template) + + template.paint_uniform_color([1, 0, 0]) + source.paint_uniform_color([0, 1, 0]) + masked_template.paint_uniform_color([0, 0, 1]) + + o3d.visualization.draw_geometries([template, source]) + o3d.visualization.draw_geometries([masked_template, source]) + +def evaluate_metrics(TP, FP, FN, TN, gt_mask): + # TP, FP, FN, TN: True +ve, False +ve, False -ve, True -ve + # gt_mask: Ground Truth mask [Nt, 1] + + accuracy = (TP + TN)/gt_mask.shape[1] + misclassification_rate = (FN + FP)/gt_mask.shape[1] + # Precision: (What portion of positive identifications are actually correct?) + precision = TP / (TP + FP) + # Recall: (What portion of actual positives are identified correctly?) + recall = TP / (TP + FN) + + fscore = (2*precision*recall) / (precision + recall) + return accuracy, precision, recall, fscore + +# Function used to evaluate the predicted mask with ground truth mask. +def evaluate_mask(gt_mask, predicted_mask, predicted_mask_idx): + # gt_mask: Ground Truth Mask [Nt, 1] + # predicted_mask: Mask predicted by network [Nt, 1] + # predicted_mask_idx: Point indices chosen by network [Ns, 1] + + if torch.is_tensor(gt_mask): gt_mask = gt_mask.detach().cpu().numpy() + if torch.is_tensor(gt_mask): predicted_mask = predicted_mask.detach().cpu().numpy() + if torch.is_tensor(predicted_mask_idx): predicted_mask_idx = predicted_mask_idx.detach().cpu().numpy() + gt_mask, predicted_mask, predicted_mask_idx = gt_mask.reshape(1,-1), predicted_mask.reshape(1,-1), predicted_mask_idx.reshape(1,-1) + + gt_idx = np.where(gt_mask == 1)[1].reshape(1,-1) # Find indices of points which are actually in source. + + # TP + FP = number of source points. + TP = np.intersect1d(predicted_mask_idx[0], gt_idx[0]).shape[0] # is inliner and predicted as inlier (True Positive) (Find common indices in predicted_mask_idx, gt_idx) + FP = len([x for x in predicted_mask_idx[0] if x not in gt_idx]) # isn't inlier but predicted as inlier (False Positive) + FN = FP # is inlier but predicted as outlier (False Negative) (due to binary classification) + TN = gt_mask.shape[1] - gt_idx.shape[1] - FN # is outlier and predicted as outlier (True Negative) + return evaluate_metrics(TP, FP, FN, TN, gt_mask) + +def test_one_epoch(args, model, test_loader): + model.eval() + test_loss = 0.0 + pred = 0.0 + count = 0 + precision_list = [] + + for i, data in enumerate(tqdm(test_loader)): + template, source, igt, gt_mask = data + + template = template.to(args.device) + source = source.to(args.device) + igt = igt.to(args.device) # [source] = [igt]*[template] + gt_mask = gt_mask.to(args.device) + + masked_template, predicted_mask = model(template, source) + + # Evaluate mask based on classification metrics. + accuracy, precision, recall, fscore = evaluate_mask(gt_mask, predicted_mask, predicted_mask_idx = model.mask_idx) + precision_list.append(precision) + + # Different ways to visualize results. + display_results(template.detach().cpu().numpy()[0], source.detach().cpu().numpy()[0], masked_template.detach().cpu().numpy()[0]) + + print("Mean Precision: ", np.mean(precision_list)) + +def test(args, model, test_loader): + test_one_epoch(args, model, test_loader) + +def options(): + parser = argparse.ArgumentParser(description='MaskNet: A Fully-Convolutional Network For Inlier Estimation (Testing)') + + # settings for input data + parser.add_argument('--num_points', default=1024, type=int, + metavar='N', help='points in point-cloud (default: 1024)') + parser.add_argument('--partial_source', default=True, type=bool, + help='create partial source point cloud in dataset.') + parser.add_argument('--noise', default=False, type=bool, + help='Add noise in source point clouds.') + parser.add_argument('--outliers', default=False, type=bool, + help='Add outliers to template point cloud.') + + # settings for on testing + parser.add_argument('-j', '--workers', default=1, type=int, + metavar='N', help='number of data loading workers (default: 4)') + parser.add_argument('-b', '--test_batch_size', default=1, type=int, + metavar='N', help='test-mini-batch size (default: 1)') + parser.add_argument('--pretrained', default='learning3d/pretrained/exp_masknet/models/best_model.t7', type=str, + metavar='PATH', help='path to pretrained model file (default: null (no-use))') + parser.add_argument('--device', default='cuda:0', type=str, + metavar='DEVICE', help='use CUDA if available') + parser.add_argument('--unseen', default=False, type=bool, + help='Use first 20 categories for training and last 20 for testing') + + args = parser.parse_args() + return args + +def main(): + args = options() + torch.backends.cudnn.deterministic = True + + testset = RegistrationData('PointNetLK', ModelNet40Data(train=False, num_points=args.num_points), + partial_source=args.partial_source, noise=args.noise, + additional_params={'use_masknet': True}) + test_loader = DataLoader(testset, batch_size=args.test_batch_size, shuffle=False, drop_last=False, num_workers=args.workers) + + if not torch.cuda.is_available(): + args.device = 'cpu' + args.device = torch.device(args.device) + + # Load Pretrained MaskNet. + model = MaskNet() + if args.pretrained: + assert os.path.isfile(args.pretrained) + model.load_state_dict(torch.load(args.pretrained, map_location='cpu')) + model = model.to(args.device) + + test(args, model, test_loader) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/thirdparty/learning3d/examples/test_masknet2.py b/thirdparty/learning3d/examples/test_masknet2.py new file mode 100644 index 0000000000000000000000000000000000000000..9a0b76b1da4849c71bae276dcbbc7a102a4a57b9 --- /dev/null +++ b/thirdparty/learning3d/examples/test_masknet2.py @@ -0,0 +1,162 @@ +import open3d as o3d +import argparse +import os +import sys +import numpy +import numpy as np +import torch +import torch.utils.data +from torch.utils.data import DataLoader +from tqdm import tqdm + +# Only if the files are in example folder. +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +if BASE_DIR[-8:] == 'examples': + sys.path.append(os.path.join(BASE_DIR, os.pardir)) + os.chdir(os.path.join(BASE_DIR, os.pardir)) + +from learning3d.models import MaskNet2 +from learning3d.data_utils import RegistrationData, ModelNet40Data + +def pc2open3d(data): + if torch.is_tensor(data): data = data.detach().cpu().numpy() + if len(data.shape) == 2: + pc = o3d.geometry.PointCloud() + pc.points = o3d.utility.Vector3dVector(data) + return pc + else: + print("Error in the shape of data given to Open3D!, Shape is ", data.shape) + +def display_results(template, source, masked_template, masked_source): + template = pc2open3d(template) + source = pc2open3d(source) + masked_template = pc2open3d(masked_template) + masked_source = pc2open3d(masked_source) + + template.paint_uniform_color([1, 0, 0]) + source.paint_uniform_color([0, 1, 0]) + # masked_template.paint_uniform_color([0, 0, 1]) + masked_template.paint_uniform_color([1, 0, 0]) + masked_source.paint_uniform_color([0, 1, 0]) + + o3d.visualization.draw_geometries([template, source]) + o3d.visualization.draw_geometries([masked_template, masked_source]) + +def evaluate_metrics(TP, FP, FN, TN, gt_mask): + # TP, FP, FN, TN: True +ve, False +ve, False -ve, True -ve + # gt_mask: Ground Truth mask [Nt, 1] + + accuracy = (TP + TN)/gt_mask.shape[1] + misclassification_rate = (FN + FP)/gt_mask.shape[1] + # Precision: (What portion of positive identifications are actually correct?) + precision = TP / (TP + FP) + # Recall: (What portion of actual positives are identified correctly?) + recall = TP / (TP + FN) + + fscore = (2*precision*recall) / (precision + recall) + return accuracy, precision, recall, fscore + +# Function used to evaluate the predicted mask with ground truth mask. +def evaluate_mask(gt_mask, predicted_mask, predicted_mask_idx): + # gt_mask: Ground Truth Mask [Nt, 1] + # predicted_mask: Mask predicted by network [Nt, 1] + # predicted_mask_idx: Point indices chosen by network [Ns, 1] + + if torch.is_tensor(gt_mask): gt_mask = gt_mask.detach().cpu().numpy() + if torch.is_tensor(gt_mask): predicted_mask = predicted_mask.detach().cpu().numpy() + if torch.is_tensor(predicted_mask_idx): predicted_mask_idx = predicted_mask_idx.detach().cpu().numpy() + gt_mask, predicted_mask, predicted_mask_idx = gt_mask.reshape(1,-1), predicted_mask.reshape(1,-1), predicted_mask_idx.reshape(1,-1) + + gt_idx = np.where(gt_mask == 1)[1].reshape(1,-1) # Find indices of points which are actually in source. + + # TP + FP = number of source points. + TP = np.intersect1d(predicted_mask_idx[0], gt_idx[0]).shape[0] # is inliner and predicted as inlier (True Positive) (Find common indices in predicted_mask_idx, gt_idx) + FP = len([x for x in predicted_mask_idx[0] if x not in gt_idx]) # isn't inlier but predicted as inlier (False Positive) + FN = FP # is inlier but predicted as outlier (False Negative) (due to binary classification) + TN = gt_mask.shape[1] - gt_idx.shape[1] - FN # is outlier and predicted as outlier (True Negative) + return evaluate_metrics(TP, FP, FN, TN, gt_mask) + +def test_one_epoch(args, model, test_loader): + model.eval() + test_loss = 0.0 + pred = 0.0 + count = 0 + + for i, data in enumerate(tqdm(test_loader)): + template, source, igt, gt_template_mask, gt_source_mask = data + + template = template.to(args.device) + source = source.to(args.device) + igt = igt.to(args.device) # [source] = [igt]*[template] + gt_template_mask = gt_template_mask.to(args.device) + gt_source_mask = gt_source_mask.to(args.device) + + masked_template, masked_source, template_mask, source_mask = model(template, source) + + # TODO: Implement evaluation strategy. + ''' + Evaluate mask based on classification metrics. + accuracy, precision, recall, fscore = evaluate_mask(gt_template_mask, template_mask, predicted_mask_idx = model.mask_idx) + precision_list.append(precision) + ''' + + # Different ways to visualize results. + display_results(template.detach().cpu().numpy()[0], source.detach().cpu().numpy()[0], masked_template.detach().cpu().numpy()[0], masked_source.detach().cpu().numpy()[0]) + +def test(args, model, test_loader): + test_one_epoch(args, model, test_loader) + +def options(): + parser = argparse.ArgumentParser(description='MaskNet: A Fully-Convolutional Network For Inlier Estimation (Testing)') + + # settings for input data + parser.add_argument('--num_points', default=1024, type=int, + metavar='N', help='points in point-cloud (default: 1024)') + parser.add_argument('--partial_source', default=True, type=bool, + help='create partial source point cloud in dataset.') + parser.add_argument('--partial_template', default=True, type=bool, + help='create partial source point cloud in dataset.') + parser.add_argument('--noise', default=False, type=bool, + help='Add noise in source point clouds.') + parser.add_argument('--outliers', default=False, type=bool, + help='Add outliers to template point cloud.') + + # settings for on testing + parser.add_argument('-j', '--workers', default=1, type=int, + metavar='N', help='number of data loading workers (default: 4)') + parser.add_argument('-b', '--test_batch_size', default=1, type=int, + metavar='N', help='test-mini-batch size (default: 1)') + parser.add_argument('--pretrained', default='learning3d/pretrained/exp_masknet2/models/best_model_0.7.t7', type=str, + metavar='PATH', help='path to pretrained model file (default: null (no-use))') + parser.add_argument('--device', default='cuda:0', type=str, + metavar='DEVICE', help='use CUDA if available') + parser.add_argument('--unseen', default=False, type=bool, + help='Use first 20 categories for training and last 20 for testing') + + args = parser.parse_args() + return args + +def main(): + args = options() + torch.backends.cudnn.deterministic = True + + testset = RegistrationData('PointNetLK', ModelNet40Data(train=False, num_points=args.num_points), + partial_template=args.partial_template, partial_source=args.partial_source, + noise=args.noise, additional_params={'use_masknet': True, 'partial_point_cloud_method': 'planar_crop'}) + test_loader = DataLoader(testset, batch_size=args.test_batch_size, shuffle=False, drop_last=False, num_workers=args.workers) + + if not torch.cuda.is_available(): + args.device = 'cpu' + args.device = torch.device(args.device) + + # Load Pretrained MaskNet. + model = MaskNet2() + if args.pretrained: + assert os.path.isfile(args.pretrained) + model.load_state_dict(torch.load(args.pretrained, map_location='cpu')) + model = model.to(args.device) + + test(args, model, test_loader) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/thirdparty/learning3d/examples/test_pcn.py b/thirdparty/learning3d/examples/test_pcn.py new file mode 100644 index 0000000000000000000000000000000000000000..3a7154c9317954917387be6e20044d09cddfe9e7 --- /dev/null +++ b/thirdparty/learning3d/examples/test_pcn.py @@ -0,0 +1,118 @@ +# author: Vinit Sarode (vinitsarode5@gmail.com) 03/23/2020 + +import open3d as o3d +import argparse +import os +import sys +import logging +import numpy +import numpy as np +import torch +import torch.utils.data +import torchvision +from torch.utils.data import DataLoader +from tensorboardX import SummaryWriter +from tqdm import tqdm + +# Only if the files are in example folder. +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +if BASE_DIR[-8:] == 'examples': + sys.path.append(os.path.join(BASE_DIR, os.pardir)) + os.chdir(os.path.join(BASE_DIR, os.pardir)) + +from learning3d.models import PCN +from learning3d.data_utils import ModelNet40Data, ClassificationData +from learning3d.losses import ChamferDistanceLoss + +def display_open3d(input_pc, output): + input_pc_ = o3d.geometry.PointCloud() + output_ = o3d.geometry.PointCloud() + input_pc_.points = o3d.utility.Vector3dVector(input_pc) + output_.points = o3d.utility.Vector3dVector(output + np.array([1,0,0])) + input_pc_.paint_uniform_color([1, 0, 0]) + output_.paint_uniform_color([0, 1, 0]) + o3d.visualization.draw_geometries([input_pc_, output_]) + +def test_one_epoch(device, model, test_loader): + model.eval() + test_loss = 0.0 + pred = 0.0 + count = 0 + for i, data in enumerate(tqdm(test_loader)): + points, _ = data + + points = points.to(device) + + output = model(points) + loss_val = ChamferDistanceLoss()(points, output['coarse_output']) + print("Loss Val: ", loss_val) + display_open3d(points[0].detach().cpu().numpy(), output['coarse_output'][0].detach().cpu().numpy()) + + test_loss += loss_val.item() + count += 1 + + test_loss = float(test_loss)/count + return test_loss + +def test(args, model, test_loader): + test_loss = test_one_epoch(args.device, model, test_loader) + +def options(): + parser = argparse.ArgumentParser(description='Point Completion Network') + parser.add_argument('--exp_name', type=str, default='exp_pcn', metavar='N', + help='Name of the experiment') + parser.add_argument('--dataset_path', type=str, default='ModelNet40', + metavar='PATH', help='path to the input dataset') # like '/path/to/ModelNet40' + parser.add_argument('--eval', type=bool, default=False, help='Train or Evaluate the network.') + + # settings for input data + parser.add_argument('--dataset_type', default='modelnet', choices=['modelnet', 'shapenet2'], + metavar='DATASET', help='dataset type (default: modelnet)') + parser.add_argument('--num_points', default=1024, type=int, + metavar='N', help='points in point-cloud (default: 1024)') + + # settings for PCN + parser.add_argument('--emb_dims', default=1024, type=int, + metavar='K', help='dim. of the feature vector (default: 1024)') + parser.add_argument('--detailed_output', default=False, type=bool, + help='Coarse + Fine Output') + + # settings for on training + parser.add_argument('--seed', type=int, default=1234) + parser.add_argument('-j', '--workers', default=4, type=int, + metavar='N', help='number of data loading workers (default: 4)') + parser.add_argument('-b', '--batch_size', default=32, type=int, + metavar='N', help='mini-batch size (default: 32)') + parser.add_argument('--pretrained', default='learning3d/pretrained/exp_pcn/models/best_model.t7', type=str, + metavar='PATH', help='path to pretrained model file (default: null (no-use))') + parser.add_argument('--device', default='cuda:0', type=str, + metavar='DEVICE', help='use CUDA if available') + + args = parser.parse_args() + return args + +def main(): + args = options() + args.dataset_path = os.path.join(os.getcwd(), os.pardir, os.pardir, 'ModelNet40', 'ModelNet40') + + trainset = ClassificationData(ModelNet40Data(train=True)) + testset = ClassificationData(ModelNet40Data(train=False)) + train_loader = DataLoader(trainset, batch_size=args.batch_size, shuffle=True, drop_last=True, num_workers=args.workers) + test_loader = DataLoader(testset, batch_size=args.batch_size, shuffle=False, drop_last=False, num_workers=args.workers) + + if not torch.cuda.is_available(): + args.device = 'cpu' + args.device = torch.device(args.device) + + # Create PointNet Model. + model = PCN(emb_dims=args.emb_dims, detailed_output=args.detailed_output) + + if args.pretrained: + assert os.path.isfile(args.pretrained) + model.load_state_dict(torch.load(args.pretrained, map_location='cpu')) + model.to(args.device) + + test(args, model, test_loader) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/thirdparty/learning3d/examples/test_pcrnet.py b/thirdparty/learning3d/examples/test_pcrnet.py new file mode 100644 index 0000000000000000000000000000000000000000..65014f269c244d916cefa081e8f7feee79c4a85f --- /dev/null +++ b/thirdparty/learning3d/examples/test_pcrnet.py @@ -0,0 +1,120 @@ +import open3d as o3d +import argparse +import os +import sys +import logging +import numpy +import numpy as np +import torch +import torch.utils.data +import torchvision +from torch.utils.data import DataLoader +from tensorboardX import SummaryWriter +from tqdm import tqdm + +# Only if the files are in example folder. +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +if BASE_DIR[-8:] == 'examples': + sys.path.append(os.path.join(BASE_DIR, os.pardir)) + os.chdir(os.path.join(BASE_DIR, os.pardir)) + +from learning3d.models import PointNet, iPCRNet +from learning3d.losses import ChamferDistanceLoss +from learning3d.data_utils import RegistrationData, ModelNet40Data + + +def display_open3d(template, source, transformed_source): + template_ = o3d.geometry.PointCloud() + source_ = o3d.geometry.PointCloud() + transformed_source_ = o3d.geometry.PointCloud() + template_.points = o3d.utility.Vector3dVector(template) + source_.points = o3d.utility.Vector3dVector(source + np.array([0,0,0])) + transformed_source_.points = o3d.utility.Vector3dVector(transformed_source) + template_.paint_uniform_color([1, 0, 0]) + source_.paint_uniform_color([0, 1, 0]) + transformed_source_.paint_uniform_color([0, 0, 1]) + o3d.visualization.draw_geometries([template_, source_, transformed_source_]) + +def test_one_epoch(device, model, test_loader): + model.eval() + test_loss = 0.0 + pred = 0.0 + count = 0 + for i, data in enumerate(tqdm(test_loader)): + template, source, igt = data + + template = template.to(device) + source = source.to(device) + igt = igt.to(device) + + output = model(template, source) + display_open3d(template.detach().cpu().numpy()[0], source.detach().cpu().numpy()[0], output['transformed_source'].detach().cpu().numpy()[0]) + loss_val = ChamferDistanceLoss()(template, output['transformed_source']) + + test_loss += loss_val.item() + count += 1 + + test_loss = float(test_loss)/count + return test_loss + +def test(args, model, test_loader): + test_loss, test_accuracy = test_one_epoch(args.device, model, test_loader) + + +def options(): + parser = argparse.ArgumentParser(description='Point Cloud Registration') + parser.add_argument('--exp_name', type=str, default='exp_ipcrnet', metavar='N', + help='Name of the experiment') + parser.add_argument('--dataset_path', type=str, default='ModelNet40', + metavar='PATH', help='path to the input dataset') # like '/path/to/ModelNet40' + parser.add_argument('--eval', type=bool, default=False, help='Train or Evaluate the network.') + + # settings for input data + parser.add_argument('--dataset_type', default='modelnet', choices=['modelnet', 'shapenet2'], + metavar='DATASET', help='dataset type (default: modelnet)') + parser.add_argument('--num_points', default=1024, type=int, + metavar='N', help='points in point-cloud (default: 1024)') + + # settings for PointNet + parser.add_argument('--emb_dims', default=1024, type=int, + metavar='K', help='dim. of the feature vector (default: 1024)') + parser.add_argument('--symfn', default='max', choices=['max', 'avg'], + help='symmetric function (default: max)') + + # settings for on training + parser.add_argument('-j', '--workers', default=4, type=int, + metavar='N', help='number of data loading workers (default: 4)') + parser.add_argument('-b', '--batch_size', default=20, type=int, + metavar='N', help='mini-batch size (default: 32)') + parser.add_argument('--pretrained', default='learning3d/pretrained/exp_ipcrnet/models/best_model.t7', type=str, + metavar='PATH', help='path to pretrained model file (default: null (no-use))') + parser.add_argument('--device', default='cuda:0', type=str, + metavar='DEVICE', help='use CUDA if available') + + args = parser.parse_args() + return args + +def main(): + args = options() + + testset = RegistrationData('PCRNet', ModelNet40Data(train=False)) + test_loader = DataLoader(testset, batch_size=args.batch_size, shuffle=False, drop_last=False, num_workers=args.workers) + + if not torch.cuda.is_available(): + args.device = 'cpu' + args.device = torch.device(args.device) + + # Create PointNet Model. + ptnet = PointNet(emb_dims=args.emb_dims) + model = iPCRNet(feature_model=ptnet) + model = model.to(args.device) + + if args.pretrained: + assert os.path.isfile(args.pretrained) + model.load_state_dict(torch.load(args.pretrained, map_location='cpu')) + model.to(args.device) + + test(args, model, test_loader) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/thirdparty/learning3d/examples/test_pnlk.py b/thirdparty/learning3d/examples/test_pnlk.py new file mode 100644 index 0000000000000000000000000000000000000000..5cb84a7e2b7cc02e4723081dfb5c902d0add3d5d --- /dev/null +++ b/thirdparty/learning3d/examples/test_pnlk.py @@ -0,0 +1,121 @@ +import open3d as o3d +import argparse +import os +import sys +import logging +import numpy +import numpy as np +import torch +import torch.utils.data +import torchvision +from torch.utils.data import DataLoader +from tensorboardX import SummaryWriter +from tqdm import tqdm + +# Only if the files are in example folder. +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +if BASE_DIR[-8:] == 'examples': + sys.path.append(os.path.join(BASE_DIR, os.pardir)) + os.chdir(os.path.join(BASE_DIR, os.pardir)) + +from learning3d.models import PointNet, PointNetLK +from learning3d.losses import FrobeniusNormLoss, RMSEFeaturesLoss +from learning3d.data_utils import RegistrationData, ModelNet40Data + +def display_open3d(template, source, transformed_source): + template_ = o3d.geometry.PointCloud() + source_ = o3d.geometry.PointCloud() + transformed_source_ = o3d.geometry.PointCloud() + template_.points = o3d.utility.Vector3dVector(template) + source_.points = o3d.utility.Vector3dVector(source + np.array([0,0,0])) + transformed_source_.points = o3d.utility.Vector3dVector(transformed_source) + template_.paint_uniform_color([1, 0, 0]) + source_.paint_uniform_color([0, 1, 0]) + transformed_source_.paint_uniform_color([0, 0, 1]) + o3d.visualization.draw_geometries([template_, source_, transformed_source_]) + +def test_one_epoch(device, model, test_loader): + model.eval() + test_loss = 0.0 + pred = 0.0 + count = 0 + for i, data in enumerate(tqdm(test_loader)): + template, source, igt = data + + template = template.to(device) + source = source.to(device) + igt = igt.to(device) + + output = model(template, source) + + display_open3d(template.detach().cpu().numpy()[0], source.detach().cpu().numpy()[0], output['transformed_source'].detach().cpu().numpy()[0]) + loss_val = FrobeniusNormLoss()(output['est_T'], igt) + RMSEFeaturesLoss()(output['r']) + + test_loss += loss_val.item() + count += 1 + + test_loss = float(test_loss)/count + return test_loss + +def test(args, model, test_loader): + test_loss, test_accuracy = test_one_epoch(args.device, model, test_loader) + + +def options(): + parser = argparse.ArgumentParser(description='Point Cloud Registration') + parser.add_argument('--exp_name', type=str, default='exp_pnlk_v1', metavar='N', + help='Name of the experiment') + parser.add_argument('--dataset_path', type=str, default='ModelNet40', + metavar='PATH', help='path to the input dataset') # like '/path/to/ModelNet40' + parser.add_argument('--eval', type=bool, default=False, help='Train or Evaluate the network.') + + # settings for input data + parser.add_argument('--dataset_type', default='modelnet', choices=['modelnet', 'shapenet2'], + metavar='DATASET', help='dataset type (default: modelnet)') + parser.add_argument('--num_points', default=1024, type=int, + metavar='N', help='points in point-cloud (default: 1024)') + + # settings for PointNet + parser.add_argument('--emb_dims', default=1024, type=int, + metavar='K', help='dim. of the feature vector (default: 1024)') + parser.add_argument('--symfn', default='max', choices=['max', 'avg'], + help='symmetric function (default: max)') + + # settings for on training + parser.add_argument('--seed', type=int, default=1234) + parser.add_argument('-j', '--workers', default=4, type=int, + metavar='N', help='number of data loading workers (default: 4)') + parser.add_argument('-b', '--batch_size', default=10, type=int, + metavar='N', help='mini-batch size (default: 32)') + parser.add_argument('--pretrained', default='learning3d/pretrained/exp_pnlk/models/best_model.t7', type=str, + metavar='PATH', help='path to pretrained model file (default: null (no-use))') + parser.add_argument('--device', default='cuda:0', type=str, + metavar='DEVICE', help='use CUDA if available') + + args = parser.parse_args() + return args + +def main(): + args = options() + + testset = RegistrationData('PointNetLK', ModelNet40Data(train=False)) + test_loader = DataLoader(testset, batch_size=8, shuffle=False, drop_last=False, num_workers=args.workers) + + if not torch.cuda.is_available(): + args.device = 'cpu' + args.device = torch.device(args.device) + + # Create PointNet Model. + ptnet = PointNet(emb_dims=args.emb_dims, use_bn=True) + model = PointNetLK(feature_model=ptnet) + model = model.to(args.device) + + if args.pretrained: + assert os.path.isfile(args.pretrained) + model.load_state_dict(torch.load(args.pretrained, map_location='cpu')) + model.to(args.device) + + test(args, model, test_loader) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/thirdparty/learning3d/examples/test_pointconv.py b/thirdparty/learning3d/examples/test_pointconv.py new file mode 100644 index 0000000000000000000000000000000000000000..6ded3bcc0ed39afde55ac986e3713e04b794d8cd --- /dev/null +++ b/thirdparty/learning3d/examples/test_pointconv.py @@ -0,0 +1,126 @@ +import open3d as o3d +import argparse +import os +import sys +import logging +import numpy +import numpy as np +import torch +import torch.utils.data +import torchvision +from torch.utils.data import DataLoader +from tensorboardX import SummaryWriter +from tqdm import tqdm + +# Only if the files are in example folder. +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +if BASE_DIR[-8:] == 'examples': + sys.path.append(os.path.join(BASE_DIR, os.pardir)) + os.chdir(os.path.join(BASE_DIR, os.pardir)) + +from learning3d.models import create_pointconv +from learning3d.models import Classifier +from learning3d.data_utils import ClassificationData, ModelNet40Data + +def display_open3d(template): + template_ = o3d.geometry.PointCloud() + template_.points = o3d.utility.Vector3dVector(template) + # template_.paint_uniform_color([1, 0, 0]) + o3d.visualization.draw_geometries([template_]) + +def test_one_epoch(device, model, test_loader, testset): + model.eval() + test_loss = 0.0 + pred = 0.0 + count = 0 + for i, data in enumerate(tqdm(test_loader)): + points, target = data + target = target[:,0] + + points = points.to(device) + target = target.to(device) + + output = model(points) + loss_val = torch.nn.functional.nll_loss( + torch.nn.functional.log_softmax(output, dim=1), target, size_average=False) + print("Ground Truth Label: ", testset.get_shape(target[0].item())) + print("Predicted Label: ", testset.get_shape(torch.argmax(output[0]).item())) + display_open3d(points.detach().cpu().numpy()[0]) + + test_loss += loss_val.item() + count += output.size(0) + + _, pred1 = output.max(dim=1) + ag = (pred1 == target) + am = ag.sum() + pred += am.item() + + test_loss = float(test_loss)/count + accuracy = float(pred)/count + return test_loss, accuracy + +def test(args, model, test_loader, testset): + test_loss, test_accuracy = test_one_epoch(args.device, model, test_loader, testset) + +def options(): + parser = argparse.ArgumentParser(description='Point Cloud Registration') + parser.add_argument('--dataset_path', type=str, default='ModelNet40', + metavar='PATH', help='path to the input dataset') # like '/path/to/ModelNet40' + parser.add_argument('--eval', type=bool, default=False, help='Train or Evaluate the network.') + + # settings for input data + parser.add_argument('--dataset_type', default='modelnet', choices=['modelnet', 'shapenet2'], + metavar='DATASET', help='dataset type (default: modelnet)') + parser.add_argument('--num_points', default=1024, type=int, + metavar='N', help='points in point-cloud (default: 1024)') + + # settings for PointNet + parser.add_argument('--pointnet', default='tune', type=str, choices=['fixed', 'tune'], + help='train pointnet (default: tune)') + parser.add_argument('-j', '--workers', default=4, type=int, + metavar='N', help='number of data loading workers (default: 4)') + parser.add_argument('-b', '--batch_size', default=32, type=int, + metavar='N', help='mini-batch size (default: 32)') + parser.add_argument('--emb_dims', default=1024, type=int, + metavar='K', help='dim. of the feature vector (default: 1024)') + parser.add_argument('--symfn', default='max', choices=['max', 'avg'], + help='symmetric function (default: max)') + + # settings for on training + parser.add_argument('--pretrained', default='learning3d/pretrained/exp_classifier/models/best_model.t7', type=str, + metavar='PATH', help='path to pretrained model file (default: null (no-use))') + parser.add_argument('--device', default='cuda:0', type=str, + metavar='DEVICE', help='use CUDA if available') + + args = parser.parse_args() + return args + +def main(): + args = options() + args.dataset_path = os.path.join(os.getcwd(), os.pardir, os.pardir, 'ModelNet40', 'ModelNet40') + + testset = ClassificationData(ModelNet40Data(train=False)) + test_loader = DataLoader(testset, batch_size=args.batch_size, shuffle=False, drop_last=False, num_workers=args.workers) + + if not torch.cuda.is_available(): + args.device = 'cpu' + args.device = torch.device(args.device) + + # To use pretrained model provided by authors. + # PointConv = create_pointconv(classifier=True, pretrained='path of pretrained model.') + # model = PointConv(emb_dims=args.emb_dims, classifier=True, pretrained='path of pretrained model.') + + # To use your own pretrained model. + PointConv = create_pointconv(classifier=False, pretrained=None) + ptconv = PointConv(emb_dims=args.emb_dims, classifier=True, pretrained=None) + model = Classifier(feature_model=ptconv) + + if args.pretrained: + assert os.path.isfile(args.pretrained) + model.load_state_dict(torch.load(args.pretrained, map_location='cpu')) + model.to(args.device) + + test(args, model, test_loader, testset) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/thirdparty/learning3d/examples/test_pointnet.py b/thirdparty/learning3d/examples/test_pointnet.py new file mode 100644 index 0000000000000000000000000000000000000000..acba980c2956b3e792e5971d6ffdfa1a73d8d08b --- /dev/null +++ b/thirdparty/learning3d/examples/test_pointnet.py @@ -0,0 +1,121 @@ +import open3d as o3d +import argparse +import os +import sys +import logging +import numpy +import numpy as np +import torch +import torch.utils.data +import torchvision +from torch.utils.data import DataLoader +from tensorboardX import SummaryWriter +from tqdm import tqdm + +# Only if the files are in example folder. +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +if BASE_DIR[-8:] == 'examples': + sys.path.append(os.path.join(BASE_DIR, os.pardir)) + os.chdir(os.path.join(BASE_DIR, os.pardir)) + +from learning3d.models import PointNet +from learning3d.models import Classifier +from learning3d.data_utils import ClassificationData, ModelNet40Data + +def display_open3d(template): + template_ = o3d.geometry.PointCloud() + template_.points = o3d.utility.Vector3dVector(template) + # template_.paint_uniform_color([1, 0, 0]) + o3d.visualization.draw_geometries([template_]) + +def test_one_epoch(device, model, test_loader, testset): + model.eval() + test_loss = 0.0 + pred = 0.0 + count = 0 + for i, data in enumerate(tqdm(test_loader)): + points, target = data + target = target[:,0] + + points = points.to(device) + target = target.to(device) + + output = model(points) + loss_val = torch.nn.functional.nll_loss( + torch.nn.functional.log_softmax(output, dim=1), target, size_average=False) + print("Ground Truth Label: ", testset.get_shape(target[0].item())) + print("Predicted Label: ", testset.get_shape(torch.argmax(output[0]).item())) + display_open3d(points.detach().cpu().numpy()[0]) + + test_loss += loss_val.item() + count += output.size(0) + + _, pred1 = output.max(dim=1) + ag = (pred1 == target) + am = ag.sum() + pred += am.item() + + test_loss = float(test_loss)/count + accuracy = float(pred)/count + return test_loss, accuracy + +def test(args, model, test_loader, testset): + test_loss, test_accuracy = test_one_epoch(args.device, model, test_loader, testset) + +def options(): + parser = argparse.ArgumentParser(description='Point Cloud Registration') + parser.add_argument('--dataset_path', type=str, default='ModelNet40', + metavar='PATH', help='path to the input dataset') # like '/path/to/ModelNet40' + parser.add_argument('--eval', type=bool, default=False, help='Train or Evaluate the network.') + + # settings for input data + parser.add_argument('--dataset_type', default='modelnet', choices=['modelnet', 'shapenet2'], + metavar='DATASET', help='dataset type (default: modelnet)') + parser.add_argument('--num_points', default=1024, type=int, + metavar='N', help='points in point-cloud (default: 1024)') + + # settings for PointNet + parser.add_argument('--pointnet', default='tune', type=str, choices=['fixed', 'tune'], + help='train pointnet (default: tune)') + parser.add_argument('-j', '--workers', default=4, type=int, + metavar='N', help='number of data loading workers (default: 4)') + parser.add_argument('-b', '--batch_size', default=32, type=int, + metavar='N', help='mini-batch size (default: 32)') + parser.add_argument('--emb_dims', default=1024, type=int, + metavar='K', help='dim. of the feature vector (default: 1024)') + parser.add_argument('--symfn', default='max', choices=['max', 'avg'], + help='symmetric function (default: max)') + + # settings for on training + parser.add_argument('--pretrained', default='learning3d/pretrained/exp_classifier/models/best_model.t7', type=str, + metavar='PATH', help='path to pretrained model file (default: null (no-use))') + parser.add_argument('--device', default='cuda:0', type=str, + metavar='DEVICE', help='use CUDA if available') + + args = parser.parse_args() + return args + +def main(): + args = options() + args.dataset_path = os.path.join(os.getcwd(), os.pardir, os.pardir, 'ModelNet40', 'ModelNet40') + + testset = ClassificationData(ModelNet40Data(train=False)) + test_loader = DataLoader(testset, batch_size=args.batch_size, shuffle=False, drop_last=False, num_workers=args.workers) + + if not torch.cuda.is_available(): + args.device = 'cpu' + args.device = torch.device(args.device) + + # Create PointNet Model. + ptnet = PointNet(emb_dims=args.emb_dims, use_bn=True) + model = Classifier(feature_model=ptnet) + + if args.pretrained: + assert os.path.isfile(args.pretrained) + model.load_state_dict(torch.load(args.pretrained, map_location='cpu')) + model.to(args.device) + + test(args, model, test_loader, testset) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/thirdparty/learning3d/examples/test_prnet.py b/thirdparty/learning3d/examples/test_prnet.py new file mode 100644 index 0000000000000000000000000000000000000000..d2029ae1ed6d12c176acb74ab37ad7d50a1011bc --- /dev/null +++ b/thirdparty/learning3d/examples/test_prnet.py @@ -0,0 +1,126 @@ +import open3d as o3d +import argparse +import os +import sys +import logging +import numpy +import numpy as np +import torch +import torch.utils.data +import torchvision +from torch.utils.data import DataLoader +from tensorboardX import SummaryWriter +from tqdm import tqdm + +# Only if the files are in example folder. +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +if BASE_DIR[-8:] == 'examples': + sys.path.append(os.path.join(BASE_DIR, os.pardir)) + os.chdir(os.path.join(BASE_DIR, os.pardir)) + +from learning3d.models import PRNet +from learning3d.data_utils import RegistrationData, ModelNet40Data + +def get_transformations(igt): + R_ba = igt[:, 0:3, 0:3] # Ps = R_ba * Pt + translation_ba = igt[:, 0:3, 3].unsqueeze(2) # Ps = Pt + t_ba + R_ab = R_ba.permute(0, 2, 1) # Pt = R_ab * Ps + translation_ab = -torch.bmm(R_ab, translation_ba) # Pt = Ps + t_ab + return R_ab, translation_ab, R_ba, translation_ba + +def display_open3d(template, source, transformed_source): + template_ = o3d.geometry.PointCloud() + source_ = o3d.geometry.PointCloud() + transformed_source_ = o3d.geometry.PointCloud() + template_.points = o3d.utility.Vector3dVector(template) + source_.points = o3d.utility.Vector3dVector(source + np.array([0,0,0])) + transformed_source_.points = o3d.utility.Vector3dVector(transformed_source) + template_.paint_uniform_color([1, 0, 0]) + source_.paint_uniform_color([0, 1, 0]) + transformed_source_.paint_uniform_color([0, 0, 1]) + o3d.visualization.draw_geometries([template_, source_, transformed_source_]) + +def test_one_epoch(device, model, test_loader): + model.eval() + test_loss = 0.0 + pred = 0.0 + count = 0 + for i, data in enumerate(tqdm(test_loader)): + template, source, igt = data + + transformations = get_transformations(igt) + transformations = [t.to(device) for t in transformations] + R_ab, translation_ab, R_ba, translation_ba = transformations + + template = template.to(device) + source = source.to(device) + igt = igt.to(device) + + output = model(template, source, R_ab, translation_ab.squeeze(2)) + display_open3d(template.detach().cpu().numpy()[0], source.detach().cpu().numpy()[0], output['transformed_source'].detach().cpu().numpy()[0]) + + test_loss += output['loss'].item() + count += 1 + + test_loss = float(test_loss)/count + return test_loss + +def test(args, model, test_loader): + test_loss = test_one_epoch(args.device, model, test_loader) + +def options(): + parser = argparse.ArgumentParser(description='Point Cloud Registration') + parser.add_argument('--exp_name', type=str, default='exp_prnet', metavar='N', + help='Name of the experiment') + parser.add_argument('--dataset_path', type=str, default='ModelNet40', + metavar='PATH', help='path to the input dataset') # like '/path/to/ModelNet40' + parser.add_argument('--eval', type=bool, default=False, help='Train or Evaluate the network.') + + # settings for input data + parser.add_argument('--dataset_type', default='modelnet', choices=['modelnet', 'shapenet2'], + metavar='DATASET', help='dataset type (default: modelnet)') + + # settings for PointNet + parser.add_argument('--emb_dims', default=512, type=int, + metavar='K', help='dim. of the feature vector (default: 1024)') + parser.add_argument('--num_iterations', default=3, type=int, + help='Number of Iterations') + + parser.add_argument('-j', '--workers', default=4, type=int, + metavar='N', help='number of data loading workers (default: 4)') + parser.add_argument('-b', '--batch_size', default=1, type=int, + metavar='N', help='mini-batch size (default: 32)') + parser.add_argument('--pretrained', default='learning3d/pretrained/exp_prnet/models/best_model.t7', type=str, + metavar='PATH', help='path to pretrained model file (default: null (no-use))') + parser.add_argument('--device', default='cuda:0', type=str, + metavar='DEVICE', help='use CUDA if available') + + args = parser.parse_args() + return args + +def main(): + args = options() + torch.backends.cudnn.deterministic = True + + trainset = RegistrationData('PRNet', ModelNet40Data(train=True), partial_source=True, partial_template=True) + testset = RegistrationData('PRNet', ModelNet40Data(train=False), partial_source=True, partial_template=True) + train_loader = DataLoader(trainset, batch_size=args.batch_size, shuffle=True, drop_last=True, num_workers=args.workers) + test_loader = DataLoader(testset, batch_size=args.batch_size, shuffle=False, drop_last=False, num_workers=args.workers) + + if not torch.cuda.is_available(): + args.device = 'cpu' + args.device = torch.device(args.device) + + # Create PointNet Model. + model = PRNet(emb_dims=args.emb_dims, num_iters=args.num_iterations) + model = model.to(args.device) + + if args.pretrained: + assert os.path.isfile(args.pretrained) + model.load_state_dict(torch.load(args.pretrained, map_location='cpu'), strict=False) + model.to(args.device) + + test(args, model, test_loader) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/thirdparty/learning3d/examples/test_rpmnet.py b/thirdparty/learning3d/examples/test_rpmnet.py new file mode 100644 index 0000000000000000000000000000000000000000..3ffc4ffe2dc01691bc80d80577972f138fc52601 --- /dev/null +++ b/thirdparty/learning3d/examples/test_rpmnet.py @@ -0,0 +1,120 @@ +import open3d as o3d +import argparse +import os +import sys +import logging +import numpy +import numpy as np +import torch +import torch.utils.data +import torchvision +from torch.utils.data import DataLoader +from tensorboardX import SummaryWriter +from tqdm import tqdm + +# Only if the files are in example folder. +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +if BASE_DIR[-8:] == 'examples': + sys.path.append(os.path.join(BASE_DIR, os.pardir)) + os.chdir(os.path.join(BASE_DIR, os.pardir)) + +from learning3d.models import RPMNet, PPFNet +from learning3d.losses import FrobeniusNormLoss, RMSEFeaturesLoss +from learning3d.data_utils import RegistrationData, ModelNet40Data + +def display_open3d(template, source, transformed_source): + template_ = o3d.geometry.PointCloud() + source_ = o3d.geometry.PointCloud() + transformed_source_ = o3d.geometry.PointCloud() + template_.points = o3d.utility.Vector3dVector(template) + source_.points = o3d.utility.Vector3dVector(source + np.array([0,0,0])) + transformed_source_.points = o3d.utility.Vector3dVector(transformed_source) + template_.paint_uniform_color([1, 0, 0]) + source_.paint_uniform_color([0, 1, 0]) + transformed_source_.paint_uniform_color([0, 0, 1]) + o3d.visualization.draw_geometries([template_, source_, transformed_source_]) + +def test_one_epoch(device, model, test_loader): + model.eval() + test_loss = 0.0 + pred = 0.0 + count = 0 + for i, data in enumerate(tqdm(test_loader)): + template, source, igt = data + + template = template.to(device) + source = source.to(device) + igt = igt.to(device) + + output = model(template, source) + + display_open3d(template.detach().cpu().numpy()[0,:,:3], source.detach().cpu().numpy()[0,:,:3], output['transformed_source'].detach().cpu().numpy()[0]) + loss_val = FrobeniusNormLoss()(output['est_T'], igt) + + test_loss += loss_val.item() + count += 1 + + test_loss = float(test_loss)/count + return test_loss + +def test(args, model, test_loader): + test_loss, test_accuracy = test_one_epoch(args.device, model, test_loader) + + +def options(): + parser = argparse.ArgumentParser(description='Point Cloud Registration') + parser.add_argument('--exp_name', type=str, default='exp_rpmnet', metavar='N', + help='Name of the experiment') + parser.add_argument('--dataset_path', type=str, default='ModelNet40', + metavar='PATH', help='path to the input dataset') # like '/path/to/ModelNet40' + parser.add_argument('--eval', type=bool, default=False, help='Train or Evaluate the network.') + + # settings for input data + parser.add_argument('--dataset_type', default='modelnet', choices=['modelnet', 'shapenet2'], + metavar='DATASET', help='dataset type (default: modelnet)') + parser.add_argument('--num_points', default=1024, type=int, + metavar='N', help='points in point-cloud (default: 1024)') + + # settings for PointNet + parser.add_argument('--emb_dims', default=1024, type=int, + metavar='K', help='dim. of the feature vector (default: 1024)') + parser.add_argument('--symfn', default='max', choices=['max', 'avg'], + help='symmetric function (default: max)') + + # settings for on training + parser.add_argument('--seed', type=int, default=1234) + parser.add_argument('-j', '--workers', default=4, type=int, + metavar='N', help='number of data loading workers (default: 4)') + parser.add_argument('-b', '--batch_size', default=10, type=int, + metavar='N', help='mini-batch size (default: 32)') + parser.add_argument('--pretrained', default='learning3d/pretrained/exp_rpmnet/models/partial-trained.pth', type=str, + metavar='PATH', help='path to pretrained model file (default: null (no-use))') + parser.add_argument('--device', default='cuda:0', type=str, + metavar='DEVICE', help='use CUDA if available') + + args = parser.parse_args() + return args + +def main(): + args = options() + + testset = RegistrationData('RPMNet', ModelNet40Data(train=False, num_points=args.num_points, use_normals=True), partial_source=True, partial_template=False) + test_loader = DataLoader(testset, batch_size=1, shuffle=False, drop_last=False, num_workers=args.workers) + + if not torch.cuda.is_available(): + args.device = 'cpu' + args.device = torch.device(args.device) + + # Create RPMNet Model. + model = RPMNet(feature_model=PPFNet()) + model = model.to(args.device) + + if args.pretrained: + assert os.path.isfile(args.pretrained) + model.load_state_dict(torch.load(args.pretrained, map_location='cpu')['state_dict']) + model.to(args.device) + + test(args, model, test_loader) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/thirdparty/learning3d/examples/train_PointNetLK.py b/thirdparty/learning3d/examples/train_PointNetLK.py new file mode 100644 index 0000000000000000000000000000000000000000..4f9c35c6f8c199e25fa32be679fab281311c48f8 --- /dev/null +++ b/thirdparty/learning3d/examples/train_PointNetLK.py @@ -0,0 +1,240 @@ +import argparse +import os +import sys +import logging +import numpy +import numpy as np +import torch +import torch.utils.data +import torchvision +from torch.utils.data import DataLoader +from tensorboardX import SummaryWriter +from tqdm import tqdm + +# Only if the files are in example folder. +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +if BASE_DIR[-8:] == 'examples': + sys.path.append(os.path.join(BASE_DIR, os.pardir)) + os.chdir(os.path.join(BASE_DIR, os.pardir)) + +from learning3d.models import PointNet +from learning3d.models import PointNetLK +from learning3d.losses import FrobeniusNormLoss, RMSEFeaturesLoss +from learning3d.data_utils import RegistrationData, ModelNet40Data + +def _init_(args): + if not os.path.exists('checkpoints'): + os.makedirs('checkpoints') + if not os.path.exists('checkpoints/' + args.exp_name): + os.makedirs('checkpoints/' + args.exp_name) + if not os.path.exists('checkpoints/' + args.exp_name + '/' + 'models'): + os.makedirs('checkpoints/' + args.exp_name + '/' + 'models') + os.system('cp main.py checkpoints' + '/' + args.exp_name + '/' + 'main.py.backup') + os.system('cp model.py checkpoints' + '/' + args.exp_name + '/' + 'model.py.backup') + + +class IOStream: + def __init__(self, path): + self.f = open(path, 'a') + + def cprint(self, text): + print(text) + self.f.write(text + '\n') + self.f.flush() + + def close(self): + self.f.close() + +def test_one_epoch(device, model, test_loader): + model.eval() + test_loss = 0.0 + pred = 0.0 + count = 0 + for i, data in enumerate(tqdm(test_loader)): + template, source, igt = data + + template = template.to(device) + source = source.to(device) + igt = igt.to(device) + + output = model(template, source) + loss_val = FrobeniusNormLoss()(output['est_T'], igt) + RMSEFeaturesLoss()(output['r']) + + test_loss += loss_val.item() + count += 1 + + test_loss = float(test_loss)/count + return test_loss + +def test(args, model, test_loader, textio): + test_loss, test_accuracy = test_one_epoch(args.device, model, test_loader) + textio.cprint('Validation Loss: %f & Validation Accuracy: %f'%(test_loss, test_accuracy)) + +def train_one_epoch(device, model, train_loader, optimizer): + model.train() + train_loss = 0.0 + pred = 0.0 + count = 0 + for i, data in enumerate(tqdm(train_loader)): + template, source, igt = data + + template = template.to(device) + source = source.to(device) + igt = igt.to(device) + + output = model(template, source) + loss_val = FrobeniusNormLoss()(output['est_T'], igt) + RMSEFeaturesLoss()(output['r']) + # print(loss_val.item()) + + # forward + backward + optimize + optimizer.zero_grad() + loss_val.backward() + optimizer.step() + + train_loss += loss_val.item() + count += 1 + + train_loss = float(train_loss)/count + return train_loss + +def train(args, model, train_loader, test_loader, boardio, textio, checkpoint): + learnable_params = filter(lambda p: p.requires_grad, model.parameters()) + if args.optimizer == 'Adam': + optimizer = torch.optim.Adam(learnable_params) + else: + optimizer = torch.optim.SGD(learnable_params, lr=0.1) + + if checkpoint is not None: + min_loss = checkpoint['min_loss'] + optimizer.load_state_dict(checkpoint['optimizer']) + + best_test_loss = np.inf + + for epoch in range(args.start_epoch, args.epochs): + train_loss = train_one_epoch(args.device, model, train_loader, optimizer) + test_loss = test_one_epoch(args.device, model, test_loader) + + if test_lossb', R, R_gt) - 1) / 2 + cos_theta = torch.clamp(cos_theta, -1, 1) + return torch.acos(cos_theta) * 180 / math.pi + +def translation_error(t, t_gt): + return torch.norm(t - t_gt, dim=1) + +def rmse(pts, T, T_gt): + pts_pred = pts @ T[:, :3, :3].transpose(1, 2) + T[:, :3, 3].unsqueeze(1) + pts_gt = pts @ T_gt[:, :3, :3].transpose(1, 2) + T_gt[:, :3, 3].unsqueeze(1) + return torch.norm(pts_pred - pts_gt, dim=2).mean(dim=1) + +def test_one_epoch(device, model, test_loader): + model.eval() + test_loss = 0.0 + pred = 0.0 + count = 0 + rotation_errors, translation_errors, rmses = [], [], [] + + for i, data in enumerate(tqdm(test_loader)): + template, source, igt = data + + template = template.to(device) + source = source.to(device) + igt = igt.to(device) + + output = model(template, source) + + eye = torch.eye(4).expand_as(igt).to(igt.device) + mse1 = F.mse_loss(output['est_T_inverse'] @ torch.inverse(igt), eye) + mse2 = F.mse_loss(output['est_T'] @ igt, eye) + loss = mse1 + mse2 + + r_err = rotation_error(est_T_inverse[:, :3, :3], igt[:, :3, :3]) + t_err = translation_error(est_T_inverse[:, :3, 3], igt[:, :3, 3]) + rmse_val = rmse(template[:, :100], est_T_inverse, igt) + rotation_errors.append(r_err) + translation_errors.append(t_err) + rmses.append(rmse_val) + + test_loss += loss_val.item() + count += 1 + + test_loss = float(test_loss)/count + print("Mean rotation error: {}, Mean translation error: {} and Mean RMSE: {}".format(np.mean(rotation_errors), np.mean(translation_errors), np.mean(rmses))) + return test_loss + +def test(args, model, test_loader, textio): + test_loss = test_one_epoch(args.device, model, test_loader) + textio.cprint('Validation Loss: %f'%(test_loss)) + +def train_one_epoch(device, model, train_loader, optimizer): + model.train() + train_loss = 0.0 + pred = 0.0 + count = 0 + for i, data in enumerate(tqdm(train_loader)): + template, source, igt = data + + template = template.to(device) + source = source.to(device) + igt = igt.to(device) + + output = model(template, source) + + eye = torch.eye(4).expand_as(igt).to(igt.device) + mse1 = F.mse_loss(output['est_T_inverse'] @ torch.inverse(igt), eye) + mse2 = F.mse_loss(output['est_T'] @ igt, eye) + loss = mse1 + mse2 + + # forward + backward + optimize + optimizer.zero_grad() + loss_val.backward() + optimizer.step() + + train_loss += loss_val.item() + count += 1 + + train_loss = float(train_loss)/count + return train_loss + +def train(args, model, train_loader, test_loader, boardio, textio, checkpoint): + learnable_params = filter(lambda p: p.requires_grad, model.parameters()) + if args.optimizer == 'Adam': + optimizer = torch.optim.Adam(learnable_params) + else: + optimizer = torch.optim.SGD(learnable_params, lr=0.1) + + if checkpoint is not None: + min_loss = checkpoint['min_loss'] + optimizer.load_state_dict(checkpoint['optimizer']) + + best_test_loss = np.inf + + for epoch in range(args.start_epoch, args.epochs): + train_loss = train_one_epoch(args.device, model, train_loader, optimizer) + test_loss = test_one_epoch(args.device, model, test_loader) + + if test_loss 0: + args.use_rri = True + return args + +def main(): + args = options() + torch.backends.cudnn.deterministic = True + torch.manual_seed(args.seed) + torch.cuda.manual_seed_all(args.seed) + np.random.seed(args.seed) + + boardio = SummaryWriter(log_dir='checkpoints/' + args.exp_name) + _init_(args) + + textio = IOStream('checkpoints/' + args.exp_name + '/run.log') + textio.cprint(str(args)) + + trainset = RegistrationData('DeepGMR', ModelNet40Data(train=True), additional_params={'nearest_neighbors': args.nearest_neighbors}) + testset = RegistrationData('DeepGMR', ModelNet40Data(train=False), additional_params={'nearest_neighbors': args.nearest_neighbors}) + train_loader = DataLoader(trainset, batch_size=args.batch_size, shuffle=True, drop_last=True, num_workers=args.workers) + test_loader = DataLoader(testset, batch_size=args.batch_size, shuffle=False, drop_last=False, num_workers=args.workers) + + if not torch.cuda.is_available(): + args.device = 'cpu' + args.device = torch.device(args.device) + + model = DeepGMR(use_rri=args.use_rri, nearest_neighbors=args.nearest_neighbors) + model = model.to(args.device) + + checkpoint = None + if args.resume: + assert os.path.isfile(args.resume) + checkpoint = torch.load(args.resume) + args.start_epoch = checkpoint['epoch'] + model.load_state_dict(checkpoint['model']) + + if args.pretrained: + assert os.path.isfile(args.pretrained) + model.load_state_dict(torch.load(args.pretrained), strict=False) + model.to(args.device) + + if args.eval: + test(args, model, test_loader, textio) + else: + train(args, model, train_loader, test_loader, boardio, textio, checkpoint) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/thirdparty/learning3d/examples/train_flownet.py b/thirdparty/learning3d/examples/train_flownet.py new file mode 100644 index 0000000000000000000000000000000000000000..45a39cd217b3506dcb0b4b9b92ce5bdf8d143f22 --- /dev/null +++ b/thirdparty/learning3d/examples/train_flownet.py @@ -0,0 +1,259 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + + +from __future__ import print_function +import os +import gc +import argparse +import torch +import torch.nn as nn +import torch.nn.functional as F +import torch.optim as optim +from torch.optim.lr_scheduler import MultiStepLR +from learning3d.models import FlowNet3D +from learning3d.data_utils import SceneflowDataset +import numpy as np +from torch.utils.data import DataLoader +from tensorboardX import SummaryWriter +from tqdm import tqdm + +class IOStream: + def __init__(self, path): + self.f = open(path, 'a') + + def cprint(self, text): + print(text) + self.f.write(text + '\n') + self.f.flush() + + def close(self): + self.f.close() + + +def _init_(args): + if not os.path.exists('checkpoints'): + os.makedirs('checkpoints') + if not os.path.exists('checkpoints/' + args.exp_name): + os.makedirs('checkpoints/' + args.exp_name) + if not os.path.exists('checkpoints/' + args.exp_name + '/' + 'models'): + os.makedirs('checkpoints/' + args.exp_name + '/' + 'models') + +def weights_init(m): + classname=m.__class__.__name__ + if classname.find('Conv2d') != -1: + nn.init.kaiming_normal_(m.weight.data) + if classname.find('Conv1d') != -1: + nn.init.kaiming_normal_(m.weight.data) + +def test_one_epoch(args, net, test_loader): + net.eval() + + total_loss = 0 + num_examples = 0 + for i, data in tqdm(enumerate(test_loader), total=len(test_loader), smoothing=0.9): + pc1, pc2, color1, color2, flow, mask1 = data + pc1 = pc1.cuda().transpose(2,1).contiguous() + pc2 = pc2.cuda().transpose(2,1).contiguous() + color1 = color1.cuda().transpose(2,1).contiguous() + color2 = color2.cuda().transpose(2,1).contiguous() + flow = flow.cuda() + mask1 = mask1.cuda().float() + + batch_size = pc1.size(0) + num_examples += batch_size + flow_pred = net(pc1, pc2, color1, color2).permute(0,2,1) + loss_1 = torch.mean(mask1 * torch.sum((flow_pred - flow) * (flow_pred - flow), -1) / 2.0) + + pc1, pc2 = pc1.permute(0,2,1), pc2.permute(0,2,1) + pc1_ = pc1 + flow_pred + + total_loss += loss_1.item() * batch_size + + + return total_loss * 1.0 / num_examples + + +def train_one_epoch(args, net, train_loader, opt): + net.train() + num_examples = 0 + total_loss = 0 + for i, data in tqdm(enumerate(train_loader), total=len(train_loader), smoothing=0.9): + pc1, pc2, color1, color2, flow, mask1 = data + pc1 = pc1.cuda().transpose(2,1).contiguous() + pc2 = pc2.cuda().transpose(2,1).contiguous() + color1 = color1.cuda().transpose(2,1).contiguous() + color2 = color2.cuda().transpose(2,1).contiguous() + flow = flow.cuda().transpose(2,1).contiguous() + mask1 = mask1.cuda().float() + + batch_size = pc1.size(0) + opt.zero_grad() + num_examples += batch_size + flow_pred = net(pc1, pc2, color1, color2) + loss_1 = torch.mean(mask1 * torch.sum((flow_pred - flow) ** 2, 1) / 2.0) + + pc1, pc2, flow_pred = pc1.permute(0,2,1), pc2.permute(0,2,1), flow_pred.permute(0,2,1) + pc1_ = pc1 + flow_pred + + loss_1.backward() + + opt.step() + total_loss += loss_1.item() * batch_size + + # if (i+1) % 100 == 0: + # print("batch: %d, mean loss: %f" % (i, total_loss / 100 / batch_size)) + # total_loss = 0 + return total_loss * 1.0 / num_examples + + +def test(args, net, test_loader, boardio, textio): + + test_loss = test_one_epoch(args, net, test_loader) + + textio.cprint('==FINAL TEST==') + textio.cprint('mean test loss: %f'%test_loss) + + +def train(args, net, train_loader, test_loader, boardio, textio): + if args.use_sgd: + print("Use SGD") + opt = optim.SGD(net.parameters(), lr=args.lr * 100, momentum=args.momentum, weight_decay=1e-4) + else: + print("Use Adam") + opt = optim.Adam(net.parameters(), lr=args.lr, weight_decay=1e-4) + scheduler = MultiStepLR(opt, milestones=[75, 150, 200], gamma=0.1) + + best_test_loss = np.inf + for epoch in range(args.epochs): + scheduler.step() + textio.cprint('==epoch: %d=='%epoch) + train_loss = train_one_epoch(args, net, train_loader, opt) + textio.cprint('mean train EPE loss: %f'%train_loss) + + test_loss = test_one_epoch(args, net, test_loader) + textio.cprint('mean test EPE loss: %f'%test_loss) + + if best_test_loss >= test_loss: + best_test_loss = test_loss + textio.cprint('best test loss till now: %f'%test_loss) + if torch.cuda.device_count() > 1: + torch.save(net.module.state_dict(), 'checkpoints/%s/models/model.best.t7' % args.exp_name) + else: + torch.save(net.state_dict(), 'checkpoints/%s/models/model.best.t7' % args.exp_name) + + boardio.add_scalar('Train Loss', train_loss, epoch+1) + boardio.add_scalar('Test Loss', test_loss, epoch+1) + boardio.add_scalar('Best Test Loss', best_test_loss, epoch+1) + + if torch.cuda.device_count() > 1: + torch.save(net.module.state_dict(), 'checkpoints/%s/models/model.%d.t7' % (args.exp_name, epoch)) + else: + torch.save(net.state_dict(), 'checkpoints/%s/models/model.%d.t7' % (args.exp_name, epoch)) + gc.collect() + + +def main(): + parser = argparse.ArgumentParser(description='Point Cloud Registration') + parser.add_argument('--exp_name', type=str, default='exp_flownet', metavar='N', + help='Name of the experiment') + parser.add_argument('--model', type=str, default='flownet', metavar='N', + choices=['flownet'], + help='Model to use, [flownet]') + parser.add_argument('--emb_dims', type=int, default=512, metavar='N', + help='Dimension of embeddings') + parser.add_argument('--num_points', type=int, default=2048, + help='Point Number [default: 2048]') + parser.add_argument('--dropout', type=float, default=0.5, metavar='N', + help='Dropout ratio in transformer') + parser.add_argument('--batch_size', type=int, default=16, metavar='batch_size', + help='Size of batch)') + parser.add_argument('--test_batch_size', type=int, default=10, metavar='batch_size', + help='Size of batch)') + parser.add_argument('--epochs', type=int, default=250, metavar='N', + help='number of episode to train ') + parser.add_argument('--use_sgd', action='store_true', default=True, + help='Use SGD') + parser.add_argument('--lr', type=float, default=0.001, metavar='LR', + help='learning rate (default: 0.001, 0.1 if using sgd)') + parser.add_argument('--momentum', type=float, default=0.9, metavar='M', + help='SGD momentum (default: 0.9)') + parser.add_argument('--no_cuda', action='store_true', default=False, + help='enables CUDA training') + parser.add_argument('--seed', type=int, default=1234, metavar='S', + help='random seed (default: 1)') + parser.add_argument('--eval', action='store_true', default=False, + help='evaluate the model') + parser.add_argument('--cycle', type=bool, default=False, metavar='N', + help='Whether to use cycle consistency') + parser.add_argument('--gaussian_noise', type=bool, default=False, metavar='N', + help='Wheter to add gaussian noise') + parser.add_argument('--unseen', type=bool, default=False, metavar='N', + help='Whether to test on unseen category') + parser.add_argument('--dataset', type=str, default='SceneflowDataset', + choices=['SceneflowDataset'], metavar='N', + help='dataset to use') + parser.add_argument('--dataset_path', type=str, default='data_processed_maxcut_35_20k_2k_8192', metavar='N', + help='dataset to use') + parser.add_argument('--model_path', type=str, default='', metavar='N', + help='Pretrained model path') + parser.add_argument('--pretrained', type=str, default='', metavar='N', + help='Pretrained model path') + + args = parser.parse_args() + os.environ['CUDA_VISIBLE_DEVICES'] = '0' + # CUDA settings + torch.backends.cudnn.deterministic = True + torch.manual_seed(args.seed) + torch.cuda.manual_seed_all(args.seed) + np.random.seed(args.seed) + + boardio = SummaryWriter(log_dir='checkpoints/' + args.exp_name) + _init_(args) + + textio = IOStream('checkpoints/' + args.exp_name + '/run.log') + textio.cprint(str(args)) + + if args.dataset == 'SceneflowDataset': + train_loader = DataLoader( + SceneflowDataset(npoints=args.num_points, partition='train'), + batch_size=args.batch_size, shuffle=True, drop_last=True) + test_loader = DataLoader( + SceneflowDataset(npoints=args.num_points, partition='test'), + batch_size=args.test_batch_size, shuffle=False, drop_last=False) + else: + raise Exception("not implemented") + + if args.model == 'flownet': + net = FlowNet3D().cuda() + net.apply(weights_init) + if args.pretrained: + net.load_state_dict(torch.load(args.pretrained), strict=False) + print("Pretrained Model Loaded Successfully!") + if args.eval: + if args.model_path is '': + model_path = 'checkpoints' + '/' + args.exp_name + '/models/model.best.t7' + else: + model_path = args.model_path + print(model_path) + if not os.path.exists(model_path): + print("can't find pretrained model") + return + net.load_state_dict(torch.load(model_path), strict=False) + if torch.cuda.device_count() > 1: + net = nn.DataParallel(net) + print("Let's use", torch.cuda.device_count(), "GPUs!") + else: + raise Exception('Not implemented') + if args.eval: + test(args, net, test_loader, boardio, textio) + else: + train(args, net, train_loader, test_loader, boardio, textio) + + + print('FINISH') + # boardio.close() + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/thirdparty/learning3d/examples/train_masknet.py b/thirdparty/learning3d/examples/train_masknet.py new file mode 100644 index 0000000000000000000000000000000000000000..dee0831665b498a6f0c174ee86327792a4c2491e --- /dev/null +++ b/thirdparty/learning3d/examples/train_masknet.py @@ -0,0 +1,239 @@ +import argparse +import os +import sys +import logging +import numpy +import numpy as np +import torch +import torch.utils.data +import torchvision +from torch.utils.data import DataLoader +from tensorboardX import SummaryWriter +from tqdm import tqdm + +# Only if the files are in example folder. +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +if BASE_DIR[-8:] == 'examples': + sys.path.append(os.path.join(BASE_DIR, os.pardir)) + os.chdir(os.path.join(BASE_DIR, os.pardir)) + +from learning3d.models import MaskNet +from learning3d.data_utils import RegistrationData, ModelNet40Data + +def _init_(args): + if not os.path.exists('checkpoints'): + os.makedirs('checkpoints') + if not os.path.exists('checkpoints/' + args.exp_name): + os.makedirs('checkpoints/' + args.exp_name) + if not os.path.exists('checkpoints/' + args.exp_name + '/' + 'models'): + os.makedirs('checkpoints/' + args.exp_name + '/' + 'models') + os.system('cp train.py checkpoints' + '/' + args.exp_name + '/' + 'train.py.backup') + os.system('cp learning3d/models/masknet.py checkpoints' + '/' + args.exp_name + '/' + 'masknet.py.backup') + os.system('cp learning3d/data_utils/dataloaders.py checkpoints' + '/' + args.exp_name + '/' + 'dataloaders.py.backup') + + +class IOStream: + def __init__(self, path): + self.f = open(path, 'a') + + def cprint(self, text): + print(text) + self.f.write(text + '\n') + self.f.flush() + + def close(self): + self.f.close() + +def test_one_epoch(args, model, test_loader): + model.eval() + test_loss = 0.0 + pred = 0.0 + count = 0 + for i, data in enumerate(tqdm(test_loader)): + template, source, igt, gt_mask = data + + template = template.to(args.device) + source = source.to(args.device) + igt = igt.to(args.device) # [source] = [igt]*[template] + gt_mask = gt_mask.to(args.device) + + masked_template, predicted_mask = model(template, source) + + if args.loss_fn == 'mse': + loss_mask = torch.nn.functional.mse_loss(predicted_mask, gt_mask) + elif args.loss_fn == 'bce': + loss_mask = torch.nn.BCELoss()(predicted_mask, gt_mask) + + test_loss += loss_mask.item() + count += 1 + + test_loss = float(test_loss)/count + return test_loss + +def test(args, model, test_loader, textio): + test_loss, test_accuracy = test_one_epoch(args.device, model, pnlk, test_loader) + textio.cprint('Validation Loss: %f & Validation Accuracy: %f'%(test_loss, test_accuracy)) + +def train_one_epoch(args, model, train_loader, optimizer): + model.train() + train_loss = 0.0 + pred = 0.0 + count = 0 + for i, data in enumerate(tqdm(train_loader)): + template, source, igt, gt_mask = data + + template = template.to(args.device) + source = source.to(args.device) + igt = igt.to(args.device) # [source] = [igt]*[template] + gt_mask = gt_mask.to(args.device) + + masked_template, predicted_mask = model(template, source) + + if args.loss_fn == 'mse': + loss_mask = torch.nn.functional.mse_loss(predicted_mask, gt_mask) + elif args.loss_fn == 'bce': + loss_mask = torch.nn.BCELoss()(predicted_mask, gt_mask) + + # forward + backward + optimize + optimizer.zero_grad() + loss_mask.backward() + optimizer.step() + + train_loss += loss_mask.item() + count += 1 + + train_loss = float(train_loss)/count + return train_loss + +def train(args, model, train_loader, test_loader, boardio, textio, checkpoint): + learnable_params = filter(lambda p: p.requires_grad, model.parameters()) + if args.optimizer == 'Adam': + optimizer = torch.optim.Adam(learnable_params, lr=0.0001) + else: + optimizer = torch.optim.SGD(learnable_params, lr=0.1) + + if checkpoint is not None: + min_loss = checkpoint['min_loss'] + optimizer.load_state_dict(checkpoint['optimizer']) + + best_test_loss = np.inf + + for epoch in range(args.start_epoch, args.epochs): + train_loss = train_one_epoch(args, model, train_loader, optimizer) + test_loss = test_one_epoch(args, model, test_loader) + + if test_loss +Maintainer-email: Vinit Sarode +License: The MIT License + + Copyright (c) 2010-2019 Google, Inc. http://angularjs.org + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +Project-URL: Homepage, https://github.com/vinits5/learning3d +Project-URL: Repository, https://github.com/vinits5/learning3d +Project-URL: Issues, https://github.com/vinits5/learning3d/issues +Project-URL: Changelog, https://github.com/vinits5/learning3d/CHANGELOG.md +Keywords: Point Clouds,Deep Learning,3D Vision,Point Cloud Registration,Point Cloud Classification,Point Cloud Segmentation +Classifier: Programming Language :: Python :: 3 +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Requires-Python: >=3.8 +Description-Content-Type: text/markdown +License-File: LICENSE +Requires-Dist: h5py +Requires-Dist: ninja +Requires-Dist: tensorboardX +Requires-Dist: tqdm +Requires-Dist: scikit-learn +Dynamic: license-file + +

+ +

+ +# Learning3D: A Modern Library for Deep Learning on 3D Point Clouds Data. + +**[Documentation](https://github.com/vinits5/learning3d#documentation) | [Blog](https://medium.com/@vinitsarode5/learning3d-a-modern-library-for-deep-learning-on-3d-point-clouds-data-48adc1fd3e0?sk=0beb59651e5ce980243bcdfbf0859b7a) | [Demo](https://github.com/vinits5/learning3d/blob/master/examples/test_pointnet.py) | [Pypi](https://pypi.org/project/learning3d/)** + +Learning3D is an open-source library that supports the development of deep learning algorithms that deal with 3D data. The Learning3D exposes a set of state of art deep neural networks in python. A modular code has been provided for further development. We welcome contributions from the open-source community. + +## Latest News: +1. \[28 Feb, 2025\]: [CurveNet](https://github.com/tiangexiang/CurveNet) is now a part of learning3d library. +2. \[7 Apr, 2024\]: Now, learning3d is available as pypi package. +3. \[24 Oct, 2023\]: [MaskNet++](https://github.com/zhouruqin/MaskNet2) is now a part of learning3d library. +4. \[12 May, 2022\]: [ChamferDistance](https://github.com/fwilliams/fml) loss function is incorporated in learning3d. This is a purely pytorch based loss function. +5. \[24 Dec. 2020\]: [MaskNet](https://arxiv.org/pdf/2010.09185.pdf) is now ready to enhance the performance of registration algorithms in learning3d for occluded point clouds. +6. \[24 Dec. 2020\]: Loss based on the predicted and ground truth correspondences is added in learning3d after consideration of [Correspondence Matrices are Underrated](https://arxiv.org/pdf/2010.16085.pdf) paper. +7. \[24 Dec. 2020\]: [PointConv](https://arxiv.org/abs/1811.07246), latent feature estimation using convolutions on point clouds is now available in learning3d. +8. \[16 Oct. 2020\]: [DeepGMR](https://wentaoyuan.github.io/deepgmr/), registration using gaussian mixture models is now available in learning3d +9. \[14 Oct. 2020\]: Now, use your own data in learning3d. (Check out [UserData](https://github.com/vinits5/learning3d#use-your-own-data) functionality!) + +## PyPI package setup +### Setup from pypi server +``` +pip install learning3d +``` + +### Setup using code +``` +git clone https://github.com/vinits5/learning3d.git +cd learning3d +git checkout pypi_v0.1.0 +python3 -m pip install . +``` + +## Available Computer Vision Algorithms in Learning3D + +| Sr. No. | Tasks | Algorithms | +|:-------------:|:----------:|:-----| +| 1 | [Classification](https://github.com/vinits5/learning3d#use-of-classification--segmentation-network) | PointNet, DGCNN, PPFNet, [PointConv](https://github.com/vinits5/learning3d#use-of-pointconv), [CurveNet](https://github.com/tiangexiang/CurveNet) | +| 2 | [Segmentation](https://github.com/vinits5/learning3d#use-of-classification--segmentation-network) | PointNet, DGCNN | +| 3 | [Reconstruction](https://github.com/vinits5/learning3d#use-of-point-completion-network) | Point Completion Network (PCN) | +| 4 | [Registration](https://github.com/vinits5/learning3d#use-of-registration-networks) | PointNetLK, PCRNet, DCP, PRNet, RPM-Net, DeepGMR | +| 5 | [Flow Estimation](https://github.com/vinits5/learning3d#use-of-flow-estimation-network) | FlowNet3D | +| 6 | [Inlier Estimation](https://github.com/vinits5/learning3d#use-of-inlier-estimation-network-masknet) | MaskNet, [MaskNet++](https://github.com/zhouruqin/MaskNet2) | + +## Available Pretrained Models +1. PointNet +2. PCN +3. PointNetLK +4. PCRNet +5. DCP +6. PRNet +7. FlowNet3D +8. RPM-Net (clean-trained.pth, noisy-trained.pth, partial-pretrained.pth) +9. DeepGMR +10. PointConv (Download from this [link](https://github.com/DylanWusee/pointconv_pytorch/blob/master/checkpoints/checkpoint.pth)) +11. MaskNet +12. MaskNet++ / MaskNet2 +13. CurveNet + +## Available Datasets +1. ModelNet40 + +## Available Loss Functions +1. Classification Loss (Cross Entropy) +2. Registration Losses (FrobeniusNormLoss, RMSEFeaturesLoss) +3. Distance Losses (Chamfer Distance, Earth Mover's Distance) +4. Correspondence Loss (based on this [paper](https://arxiv.org/pdf/2010.16085.pdf)) + +## Technical Details +### Supported OS +1. Ubuntu 16.04 +2. Ubuntu 18.04 +3. Ubuntu 20.04.6 +4. Linux Mint +5. macOS Sequoia 15.3.1 + +### Requirements +1. CUDA 10.0 or higher +2. Pytorch 1.3 or higher +3. Python 3.8 + +## How to use this library? +**Important Note: Clone this repository in your project. Please don't add your codes in "learning3d" folder.** + +1. All networks are defined in the module "models". +2. All loss functions are defined in the module "losses". +3. Data loaders are pre-defined in data_utils/dataloaders.py file. +4. All pretrained models are provided in learning3d/pretrained folder. + +## Documentation +B: Batch Size, N: No. of points and C: Channels. +#### Use of Point Embedding Networks: +> from learning3d.models import PointNet, DGCNN, PPFNet\ +> pn = PointNet(emb_dims=1024, input_shape='bnc', use_bn=False)\ +> dgcnn = DGCNN(emb_dims=1024, input_shape='bnc')\ +> ppf = PPFNet(features=['ppf', 'dxyz', 'xyz'], emb_dims=96, radius='0.3', num_neighbours=64) + +| Sr. No. | Variable | Data type | Shape | Choices | Use | +|:---:|:---:|:---:|:---:|:---:|:---:| +| 1. | emb_dims | Integer | Scalar | 1024, 512 | Size of feature vector for the each point| +| 2. | input_shape | String | - | 'bnc', 'bcn' | Shape of input point cloud| +| 3. | output | tensor | BxCxN | - | High dimensional embeddings for each point| +| 4. | features | List of Strings | - | ['ppf', 'dxyz', 'xyz'] | Use of various features | +| 5. | radius | Float | Scalar | 0.3 | Radius of cluster for computing local features | +| 6. | num_neighbours | Integer | Scalar | 64 | Maximum number of points to consider per cluster | + +#### Use of Classification / Segmentation Network: +> from learning3d.models import Classifier, PointNet, Segmentation\ +> classifier = Classifier(feature_model=PointNet(), num_classes=40)\ +> seg = Segmentation(feature_model=PointNet(), num_classes=40) + +| Sr. No. | Variable | Data type | Shape | Choices | Use | +|:---:|:---:|:---:|:---:|:---:|:---:| +| 1. | feature_model | Object | - | PointNet / DGCNN | Point cloud embedding network | +| 2. | num_classes | Integer | Scalar | 10, 40 | Number of object categories to be classified | +| 3. | output | tensor | Classification: Bx40, Segmentation: BxNx40 | 10, 40 | Probabilities of each category or each point | + +#### Use of Registration Networks: +> from learning3d.models import PointNet, PointNetLK, DCP, iPCRNet, PRNet, PPFNet, RPMNet\ +> pnlk = PointNetLK(feature_model=PointNet(), delta=1e-02, xtol=1e-07, p0_zero_mean=True, p1_zero_mean=True, pooling='max')\ +> dcp = DCP(feature_model=PointNet(), pointer_='transformer', head='svd')\ +> pcrnet = iPCRNet(feature_moodel=PointNet(), pooling='max')\ +> rpmnet = RPMNet(feature_model=PPFNet())\ +> deepgmr = DeepGMR(use_rri=True, feature_model=PointNet(), nearest_neighbors=20) + +| Sr. No. | Variable | Data type | Choices | Use | Algorithm | +|:---:|:---:|:---:|:---:|:---:|:---:| +| 1. | feature_model | Object | PointNet / DGCNN | Point cloud embedding network | PointNetLK | +| 2. | delta | Float | Scalar | Parameter to calculate approximate jacobian | PointNetLK | +| 3. | xtol | Float | Scalar | Check tolerance to stop iterations | PointNetLK | +| 4. | p0_zero_mean | Boolean | True/False | Subtract mean from template point cloud | PointNetLK | +| 5. | p1_zero_mean | Boolean | True/False | Subtract mean from source point cloud | PointNetLK | +| 6. | pooling | String | 'max' / 'avg' | Type of pooling used to get global feature vectror | PointNetLK | +| 7. | pointer_ | String | 'transformer' / 'identity' | Choice for Transformer/Attention network | DCP | +| 8. | head | String | 'svd' / 'mlp' | Choice of module to estimate registration params | DCP | +| 9. | use_rri | Boolean | True/False | Use nearest neighbors to estimate point cloud features. | DeepGMR | +| 10. | nearest_neighbores | Integer | 20/any integer | Give number of nearest neighbors used to estimate features | DeepGMR | + +#### Use of Inlier Estimation Network (MaskNet): +> from learning3d.models import MaskNet, PointNet, MaskNet2\ +> masknet = MaskNet(feature_model=PointNet(), is_training=True) +> masknet2 = MaskNet2(feature_model=PointNet(), is_training=True) + +| Sr. No. | Variable | Data type | Choices | Use | +|:---:|:---:|:---:|:---:|:---:| +| 1. | feature_model | Object | PointNet / DGCNN | Point cloud embedding network | +| 2. | is_training | Boolean | True / False | Specify if the network will undergo training or testing | + +#### Use of Point Completion Network: +> from learning3d.models import PCN\ +> pcn = PCN(emb_dims=1024, input_shape='bnc', num_coarse=1024, grid_size=4, detailed_output=True) + +| Sr. No. | Variable | Data type | Choices | Use | +|:---:|:---:|:---:|:---:|:---:| +| 1. | emb_dims | Integer | 1024, 512 | Size of feature vector for each point | +| 2. | input_shape | String | 'bnc' / 'bcn' | Shape of input point cloud | +| 3. | num_coarse | Integer | 1024 | Shape of output point cloud | +| 4. | grid_size | Integer | 4, 8, 16 | Size of grid used to produce detailed output | +| 5. | detailed_output | Boolean | True / False | Choice for additional module to create detailed output point cloud| + +#### Use of PointConv: +Use the following to create pretrained model provided by authors. +> from learning3d.models import create_pointconv\ +> PointConv = create_pointconv(classifier=True, pretrained='path of checkpoint')\ +> ptconv = PointConv(emb_dims=1024, input_shape='bnc', input_channel_dim=6, classifier=True) + +**OR**\ +Use the following to create your own PointConv model. + +> PointConv = create_pointconv(classifier=False, pretrained=None)\ +> ptconv = PointConv(emb_dims=1024, input_shape='bnc', input_channel_dim=3, classifier=True) + +PointConv variable is a class. Users can use it to create a sub-class to override *create_classifier* and *create_structure* methods in order to change PointConv's network architecture. + +| Sr. No. | Variable | Data type | Choices | Use | +|:---:|:---:|:---:|:---:|:---:| +| 1. | emb_dims | Integer | 1024, 512 | Size of feature vector for each point | +| 2. | input_shape | String | 'bnc' / 'bcn' | Shape of input point cloud | +| 3. | input_channel_dim | Integer | 3/6 | Define if point cloud contains only xyz co-ordinates or normals and colors as well | +| 4. | classifier | Boolean | True / False | Choose if you want to use a classifier with PointConv | +| 5. | pretrained | Boolean | String | Give path of the pretrained classifier model (only use it for weights given by authors) | + +#### Use of Flow Estimation Network: +> from learning3d.models import FlowNet3D\ +> flownet = FlowNet3D() + +#### Use of Data Loaders: +> from learning3d.data_utils import ModelNet40Data, ClassificationData, RegistrationData, FlowData\ +> modelnet40 = ModelNet40Data(train=True, num_points=1024, download=True)\ +> classification_data = ClassificationData(data_class=ModelNet40Data())\ +> registration_data = RegistrationData(algorithm='PointNetLK', data_class=ModelNet40Data(), partial_source=False, partial_template=False, noise=False)\ +> flow_data = FlowData() + +| Sr. No. | Variable | Data type | Choices | Use | +|:---:|:---:|:---:|:---:|:---:| +| 1. | train | Boolean | True / False | Split data as train/test set | +| 2. | num_points | Integer | 1024 | Number of points in each point cloud | +| 3. | download | Boolean | True / False | If data not available then download it | +| 4. | data_class | Object | - | Specify which dataset to use | +| 5. | algorithm | String | 'PointNetLK', 'PCRNet', 'DCP', 'iPCRNet' | Algorithm used for registration | +| 6. | partial_source | Boolean | True / False | Create partial source point cloud | +| 7. | partial_template | Boolean | True / False | Create partial template point cloud | +| 8. | noise | Boolean | True / False | Add noise in source point cloud | + +#### Use Your Own Data: +> from learning3d.data_utils import UserData\ +> dataset = UserData(application, data_dict) + +|Sr. No. | Application | Required Key | Respective Value | +|:---:|:---:|:---:|:---:| +| 1. | 'classification' | 'pcs' | Point Clouds (BxNx3) | +| | | 'labels' | Ground Truth Class Labels (BxN) | +| 2. | 'registration' | 'template' | Template Point Clouds (BxNx3) | +| | | 'source' | Source Point Clouds (BxNx3) | +| | | 'transformation' | Ground Truth Transformation (Bx4x4)| +| 3. | 'flow_estimation' | 'frame1' | Point Clouds (BxNx3) | +| | | 'frame2' | Point Clouds (BxNx3) | +| | | 'flow' | Ground Truth Flow Vector (BxNx3)| + +#### Use of Loss Functions: +> from learning3d.losses import RMSEFeaturesLoss, FrobeniusNormLoss, ClassificationLoss, EMDLoss, ChamferDistanceLoss, CorrespondenceLoss\ +> rmse = RMSEFeaturesLoss()\ +> fn_loss = FrobeniusNormLoss()\ +> classification_loss = ClassificationLoss()\ +> emd = EMDLoss()\ +> cd = ChamferDistanceLoss()\ +> corr = CorrespondenceLoss() + +| Sr. No. | Loss Type | Use | +|:---:|:---:|:---:| +| 1. | RMSEFeaturesLoss | Used to find root mean square value between two global feature vectors of point clouds | +| 2. | FrobeniusNormLoss | Used to find frobenius norm between two transfromation matrices | +| 3. | ClassificationLoss | Used to calculate cross-entropy loss | +| 4. | EMDLoss | Earth Mover's distance between two given point clouds | +| 5. | ChamferDistanceLoss | Chamfer's distance between two given point clouds | +| 6. | CorrespondenceLoss | Computes cross entropy loss using the predicted correspondence and ground truth correspondence for each source point | + +### To run codes from examples: +1. Copy the file from "examples" folder outside of the directory "learning3d" +2. Now, run the file. (ex. python test_pointnet.py) +- Your Directory/Location + - learning3d + - test_pointnet.py + +### References: +1. [PointNet:](https://arxiv.org/abs/1612.00593) Deep Learning on Point Sets for 3D Classification and Segmentation +2. [Dynamic Graph CNN](https://arxiv.org/abs/1801.07829) for Learning on Point Clouds +3. [PPFNet:](https://arxiv.org/pdf/1802.02669.pdf) Global Context Aware Local Features for Robust 3D Point Matching +4. [PointConv:](https://arxiv.org/abs/1811.07246) Deep Convolutional Networks on 3D Point Clouds +5. [PointNetLK:](https://arxiv.org/abs/1903.05711) Robust & Efficient Point Cloud Registration using PointNet +6. [PCRNet:](https://arxiv.org/abs/1908.07906) Point Cloud Registration Network using PointNet Encoding +7. [Deep Closest Point:](https://arxiv.org/abs/1905.03304) Learning Representations for Point Cloud Registration +8. [PRNet:](https://arxiv.org/abs/1910.12240) Self-Supervised Learning for Partial-to-Partial Registration +9. [FlowNet3D:](https://arxiv.org/abs/1806.01411) Learning Scene Flow in 3D Point Clouds +10. [PCN:](https://arxiv.org/pdf/1808.00671.pdf) Point Completion Network +11. [RPM-Net:](https://arxiv.org/pdf/2003.13479.pdf) Robust Point Matching using Learned Features +12. [3D ShapeNets:](https://people.csail.mit.edu/khosla/papers/cvpr2015_wu.pdf) A Deep Representation for Volumetric Shapes +13. [DeepGMR:](https://arxiv.org/abs/2008.09088) Learning Latent Gaussian Mixture Models for Registration +14. [CMU:](https://arxiv.org/pdf/2010.16085.pdf) Correspondence Matrices are Underrated +15. [MaskNet:](https://arxiv.org/pdf/2010.09185.pdf) A Fully-Convolutional Network to Estimate Inlier Points +16. [MaskNet++:](https://www.sciencedirect.com/science/article/abs/pii/S0097849322000085) Inlier/outlier identification for two point clouds +17. [CurveNet:](https://github.com/tiangexiang/CurveNet) Walk in the Cloud: Learning Curves for Point Clouds Shape Analysis diff --git a/thirdparty/learning3d/learning3d.egg-info/SOURCES.txt b/thirdparty/learning3d/learning3d.egg-info/SOURCES.txt new file mode 100644 index 0000000000000000000000000000000000000000..574cfeb3bbb42f597e4f7619c6de2d84e849662a --- /dev/null +++ b/thirdparty/learning3d/learning3d.egg-info/SOURCES.txt @@ -0,0 +1,71 @@ +LICENSE +MANIFEST.in +README.md +pyproject.toml +requirements.txt +learning3d/data_utils/__init__.py +learning3d/data_utils/dataloaders.py +learning3d/data_utils/user_data.py +learning3d/examples/test_curvenet.py +learning3d/examples/test_dcp.py +learning3d/examples/test_deepgmr.py +learning3d/examples/test_masknet.py +learning3d/examples/test_masknet2.py +learning3d/examples/test_pcn.py +learning3d/examples/test_pcrnet.py +learning3d/examples/test_pnlk.py +learning3d/examples/test_pointconv.py +learning3d/examples/test_pointnet.py +learning3d/examples/test_prnet.py +learning3d/examples/test_rpmnet.py +learning3d/examples/train_PointNetLK.py +learning3d/examples/train_dcp.py +learning3d/examples/train_deepgmr.py +learning3d/examples/train_masknet.py +learning3d/examples/train_pcn.py +learning3d/examples/train_pcrnet.py +learning3d/examples/train_pointconv.py +learning3d/examples/train_pointnet.py +learning3d/examples/train_prnet.py +learning3d/examples/train_rpmnet.py +learning3d/learning3d.egg-info/PKG-INFO +learning3d/losses/__init__.py +learning3d/losses/chamfer_distance.py +learning3d/losses/classification.py +learning3d/losses/correspondence_loss.py +learning3d/losses/emd.py +learning3d/losses/frobenius_norm.py +learning3d/losses/rmse_features.py +learning3d/models/__init__.py +learning3d/models/classifier.py +learning3d/models/curvenet.py +learning3d/models/dcp.py +learning3d/models/deepgmr.py +learning3d/models/dgcnn.py +learning3d/models/masknet.py +learning3d/models/masknet2.py +learning3d/models/pcn.py +learning3d/models/pcrnet.py +learning3d/models/pointconv.py +learning3d/models/pointnet.py +learning3d/models/pointnetlk.py +learning3d/models/pooling.py +learning3d/models/ppfnet.py +learning3d/models/prnet.py +learning3d/models/rpmnet.py +learning3d/models/segmentation.py +learning3d/ops/__init__.py +learning3d/ops/data_utils.py +learning3d/ops/invmat.py +learning3d/ops/quaternion.py +learning3d/ops/se3.py +learning3d/ops/sinc.py +learning3d/ops/so3.py +learning3d/ops/transform_functions.py +learning3d/utils/__init__.py +learning3d/utils/curvenet_util.py +learning3d/utils/model_common_utils.py +learning3d/utils/pointconv_util.py +learning3d/utils/ppfnet_util.py +learning3d/utils/svd.py +learning3d/utils/transformer.py \ No newline at end of file diff --git a/thirdparty/learning3d/learning3d.egg-info/dependency_links.txt b/thirdparty/learning3d/learning3d.egg-info/dependency_links.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/thirdparty/learning3d/learning3d.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/thirdparty/learning3d/learning3d.egg-info/requires.txt b/thirdparty/learning3d/learning3d.egg-info/requires.txt new file mode 100644 index 0000000000000000000000000000000000000000..e6093629f1f758cb91a305327988c6ec6e89643c --- /dev/null +++ b/thirdparty/learning3d/learning3d.egg-info/requires.txt @@ -0,0 +1,5 @@ +h5py +ninja +tensorboardX +tqdm +scikit-learn diff --git a/thirdparty/learning3d/learning3d.egg-info/top_level.txt b/thirdparty/learning3d/learning3d.egg-info/top_level.txt new file mode 100644 index 0000000000000000000000000000000000000000..84b607a45909120ea7f4bf5a3684afd33691ed15 --- /dev/null +++ b/thirdparty/learning3d/learning3d.egg-info/top_level.txt @@ -0,0 +1,8 @@ +data_utils +examples +images +losses +models +ops +pretrained +utils diff --git a/thirdparty/learning3d/losses/__init__.py b/thirdparty/learning3d/losses/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..bde254b85876657dffc62cdc8715fab456674390 --- /dev/null +++ b/thirdparty/learning3d/losses/__init__.py @@ -0,0 +1,12 @@ +from .rmse_features import RMSEFeaturesLoss +from .frobenius_norm import FrobeniusNormLoss +from .classification import ClassificationLoss +from .correspondence_loss import CorrespondenceLoss +try: + from .emd import EMDLoss +except: + print("Sorry EMD loss is not compatible with your system!") +try: + from .chamfer_distance import ChamferDistanceLoss +except: + print("Sorry ChamferDistance loss is not compatible with your system!") \ No newline at end of file diff --git a/thirdparty/learning3d/losses/chamfer_distance.py b/thirdparty/learning3d/losses/chamfer_distance.py new file mode 100644 index 0000000000000000000000000000000000000000..89680d099466027a4af173ee0b869f1ce0365a35 --- /dev/null +++ b/thirdparty/learning3d/losses/chamfer_distance.py @@ -0,0 +1,51 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F + +def pairwise_distances(a: torch.Tensor, b: torch.Tensor, p=2): + """ + Compute the pairwise distance_tensor matrix between a and b which both have size [m, n, d]. The result is a tensor of + size [m, n, n] whose entry [m, i, j] contains the distance_tensor between a[m, i, :] and b[m, j, :]. + :param a: A tensor containing m batches of n points of dimension d. i.e. of size [m, n, d] + :param b: A tensor containing m batches of n points of dimension d. i.e. of size [m, n, d] + :param p: Norm to use for the distance_tensor + :return: A tensor containing the pairwise distance_tensor between each pair of inputs in a batch. + """ + + if len(a.shape) != 3: + raise ValueError("Invalid shape for a. Must be [m, n, d] but got", a.shape) + if len(b.shape) != 3: + raise ValueError("Invalid shape for a. Must be [m, n, d] but got", b.shape) + return (a.unsqueeze(2) - b.unsqueeze(1)).abs().pow(p).sum(3) + +def chamfer(a, b): + """ + Compute the chamfer distance between two sets of vectors, a, and b + :param a: A m-sized minibatch of point sets in R^d. i.e. shape [m, n_a, d] + :param b: A m-sized minibatch of point sets in R^d. i.e. shape [m, n_b, d] + :return: A [m] shaped tensor storing the Chamfer distance between each minibatch entry + """ + M = pairwise_distances(a, b) + dist1 = torch.mean(torch.sqrt(M.min(1)[0])) + dist2 = torch.mean(torch.sqrt(M.min(2)[0])) + return (dist1 + dist2) / 2.0 + + +def chamfer_distance(template: torch.Tensor, source: torch.Tensor): + try: + from .cuda.chamfer_distance import ChamferDistance + cost_p0_p1, cost_p1_p0 = ChamferDistance()(template, source) + cost_p0_p1 = torch.mean(torch.sqrt(cost_p0_p1)) + cost_p1_p0 = torch.mean(torch.sqrt(cost_p1_p0)) + chamfer_loss = (cost_p0_p1 + cost_p1_p0)/2.0 + except: + chamfer_loss = chamfer(template, source) + return chamfer_loss + + +class ChamferDistanceLoss(nn.Module): + def __init__(self): + super(ChamferDistanceLoss, self).__init__() + + def forward(self, template, source): + return chamfer_distance(template, source) \ No newline at end of file diff --git a/thirdparty/learning3d/losses/classification.py b/thirdparty/learning3d/losses/classification.py new file mode 100644 index 0000000000000000000000000000000000000000..df3dbd499ad4d5550367fa127ce16da8c90d6e5a --- /dev/null +++ b/thirdparty/learning3d/losses/classification.py @@ -0,0 +1,14 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F + +def classification_loss(prediction: torch.Tensor, target: torch.Tensor): + return F.nll_loss(prediction, target) + + +class ClassificationLoss(nn.Module): + def __init__(self): + super(ClassificationLoss, self).__init__() + + def forward(self, prediction, target): + return classification_loss(prediction, target) \ No newline at end of file diff --git a/thirdparty/learning3d/losses/correspondence_loss.py b/thirdparty/learning3d/losses/correspondence_loss.py new file mode 100644 index 0000000000000000000000000000000000000000..4ec2300a3d6e7920a44b6a251283de6b052be33e --- /dev/null +++ b/thirdparty/learning3d/losses/correspondence_loss.py @@ -0,0 +1,10 @@ +import torch + +class CorrespondenceLoss(torch.nn.Module): + def forward(self, template, source, corr_mat_pred, corr_mat): + # corr_mat: batch_size x num_template x num_source (ground truth correspondence matrix) + # corr_mat_pred: batch_size x num_source x num_template (predicted correspondence matrix) + batch_size, _, num_points_template = template.shape + _, _, num_points = source.shape + return torch.nn.functional.cross_entropy(corr_mat_pred.view(batch_size*num_points, num_points_template), + torch.argmax(corr_mat.transpose(1,2).reshape(-1, num_points_template), axis=1)) \ No newline at end of file diff --git a/thirdparty/learning3d/losses/cuda/chamfer_distance/__init__.py b/thirdparty/learning3d/losses/cuda/chamfer_distance/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..2e15be7028d12ddc55b29752ac718c5284200203 --- /dev/null +++ b/thirdparty/learning3d/losses/cuda/chamfer_distance/__init__.py @@ -0,0 +1 @@ +from .chamfer_distance import ChamferDistance diff --git a/thirdparty/learning3d/losses/cuda/chamfer_distance/chamfer_distance.cpp b/thirdparty/learning3d/losses/cuda/chamfer_distance/chamfer_distance.cpp new file mode 100644 index 0000000000000000000000000000000000000000..40f3d79aee526188f2df559a34e563026933be56 --- /dev/null +++ b/thirdparty/learning3d/losses/cuda/chamfer_distance/chamfer_distance.cpp @@ -0,0 +1,185 @@ +#include + +// CUDA forward declarations +int ChamferDistanceKernelLauncher( + const int b, const int n, + const float* xyz, + const int m, + const float* xyz2, + float* result, + int* result_i, + float* result2, + int* result2_i); + +int ChamferDistanceGradKernelLauncher( + const int b, const int n, + const float* xyz1, + const int m, + const float* xyz2, + const float* grad_dist1, + const int* idx1, + const float* grad_dist2, + const int* idx2, + float* grad_xyz1, + float* grad_xyz2); + + +void chamfer_distance_forward_cuda( + const at::Tensor xyz1, + const at::Tensor xyz2, + const at::Tensor dist1, + const at::Tensor dist2, + const at::Tensor idx1, + const at::Tensor idx2) +{ + ChamferDistanceKernelLauncher(xyz1.size(0), xyz1.size(1), xyz1.data(), + xyz2.size(1), xyz2.data(), + dist1.data(), idx1.data(), + dist2.data(), idx2.data()); +} + +void chamfer_distance_backward_cuda( + const at::Tensor xyz1, + const at::Tensor xyz2, + at::Tensor gradxyz1, + at::Tensor gradxyz2, + at::Tensor graddist1, + at::Tensor graddist2, + at::Tensor idx1, + at::Tensor idx2) +{ + ChamferDistanceGradKernelLauncher(xyz1.size(0), xyz1.size(1), xyz1.data(), + xyz2.size(1), xyz2.data(), + graddist1.data(), idx1.data(), + graddist2.data(), idx2.data(), + gradxyz1.data(), gradxyz2.data()); +} + + +void nnsearch( + const int b, const int n, const int m, + const float* xyz1, + const float* xyz2, + float* dist, + int* idx) +{ + for (int i = 0; i < b; i++) { + for (int j = 0; j < n; j++) { + const float x1 = xyz1[(i*n+j)*3+0]; + const float y1 = xyz1[(i*n+j)*3+1]; + const float z1 = xyz1[(i*n+j)*3+2]; + double best = 0; + int besti = 0; + for (int k = 0; k < m; k++) { + const float x2 = xyz2[(i*m+k)*3+0] - x1; + const float y2 = xyz2[(i*m+k)*3+1] - y1; + const float z2 = xyz2[(i*m+k)*3+2] - z1; + const double d=x2*x2+y2*y2+z2*z2; + if (k==0 || d < best){ + best = d; + besti = k; + } + } + dist[i*n+j] = best; + idx[i*n+j] = besti; + } + } +} + + +void chamfer_distance_forward( + const at::Tensor xyz1, + const at::Tensor xyz2, + const at::Tensor dist1, + const at::Tensor dist2, + const at::Tensor idx1, + const at::Tensor idx2) +{ + const int batchsize = xyz1.size(0); + const int n = xyz1.size(1); + const int m = xyz2.size(1); + + const float* xyz1_data = xyz1.data(); + const float* xyz2_data = xyz2.data(); + float* dist1_data = dist1.data(); + float* dist2_data = dist2.data(); + int* idx1_data = idx1.data(); + int* idx2_data = idx2.data(); + + nnsearch(batchsize, n, m, xyz1_data, xyz2_data, dist1_data, idx1_data); + nnsearch(batchsize, m, n, xyz2_data, xyz1_data, dist2_data, idx2_data); +} + + +void chamfer_distance_backward( + const at::Tensor xyz1, + const at::Tensor xyz2, + at::Tensor gradxyz1, + at::Tensor gradxyz2, + at::Tensor graddist1, + at::Tensor graddist2, + at::Tensor idx1, + at::Tensor idx2) +{ + const int b = xyz1.size(0); + const int n = xyz1.size(1); + const int m = xyz2.size(1); + + const float* xyz1_data = xyz1.data(); + const float* xyz2_data = xyz2.data(); + float* gradxyz1_data = gradxyz1.data(); + float* gradxyz2_data = gradxyz2.data(); + float* graddist1_data = graddist1.data(); + float* graddist2_data = graddist2.data(); + const int* idx1_data = idx1.data(); + const int* idx2_data = idx2.data(); + + for (int i = 0; i < b*n*3; i++) + gradxyz1_data[i] = 0; + for (int i = 0; i < b*m*3; i++) + gradxyz2_data[i] = 0; + for (int i = 0;i < b; i++) { + for (int j = 0; j < n; j++) { + const float x1 = xyz1_data[(i*n+j)*3+0]; + const float y1 = xyz1_data[(i*n+j)*3+1]; + const float z1 = xyz1_data[(i*n+j)*3+2]; + const int j2 = idx1_data[i*n+j]; + + const float x2 = xyz2_data[(i*m+j2)*3+0]; + const float y2 = xyz2_data[(i*m+j2)*3+1]; + const float z2 = xyz2_data[(i*m+j2)*3+2]; + const float g = graddist1_data[i*n+j]*2; + + gradxyz1_data[(i*n+j)*3+0] += g*(x1-x2); + gradxyz1_data[(i*n+j)*3+1] += g*(y1-y2); + gradxyz1_data[(i*n+j)*3+2] += g*(z1-z2); + gradxyz2_data[(i*m+j2)*3+0] -= (g*(x1-x2)); + gradxyz2_data[(i*m+j2)*3+1] -= (g*(y1-y2)); + gradxyz2_data[(i*m+j2)*3+2] -= (g*(z1-z2)); + } + for (int j = 0; j < m; j++) { + const float x1 = xyz2_data[(i*m+j)*3+0]; + const float y1 = xyz2_data[(i*m+j)*3+1]; + const float z1 = xyz2_data[(i*m+j)*3+2]; + const int j2 = idx2_data[i*m+j]; + const float x2 = xyz1_data[(i*n+j2)*3+0]; + const float y2 = xyz1_data[(i*n+j2)*3+1]; + const float z2 = xyz1_data[(i*n+j2)*3+2]; + const float g = graddist2_data[i*m+j]*2; + gradxyz2_data[(i*m+j)*3+0] += g*(x1-x2); + gradxyz2_data[(i*m+j)*3+1] += g*(y1-y2); + gradxyz2_data[(i*m+j)*3+2] += g*(z1-z2); + gradxyz1_data[(i*n+j2)*3+0] -= (g*(x1-x2)); + gradxyz1_data[(i*n+j2)*3+1] -= (g*(y1-y2)); + gradxyz1_data[(i*n+j2)*3+2] -= (g*(z1-z2)); + } + } +} + + +PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) { + m.def("forward", &chamfer_distance_forward, "ChamferDistance forward"); + m.def("forward_cuda", &chamfer_distance_forward_cuda, "ChamferDistance forward (CUDA)"); + m.def("backward", &chamfer_distance_backward, "ChamferDistance backward"); + m.def("backward_cuda", &chamfer_distance_backward_cuda, "ChamferDistance backward (CUDA)"); +} diff --git a/thirdparty/learning3d/losses/cuda/chamfer_distance/chamfer_distance.cu b/thirdparty/learning3d/losses/cuda/chamfer_distance/chamfer_distance.cu new file mode 100644 index 0000000000000000000000000000000000000000..f10f2ba854883d7f590236bb69e3598e8a4ef379 --- /dev/null +++ b/thirdparty/learning3d/losses/cuda/chamfer_distance/chamfer_distance.cu @@ -0,0 +1,209 @@ +#include + +#include +#include + +__global__ +void ChamferDistanceKernel( + int b, + int n, + const float* xyz, + int m, + const float* xyz2, + float* result, + int* result_i) +{ + const int batch=512; + __shared__ float buf[batch*3]; + for (int i=blockIdx.x;ibest){ + result[(i*n+j)]=best; + result_i[(i*n+j)]=best_i; + } + } + __syncthreads(); + } + } +} + +void ChamferDistanceKernelLauncher( + const int b, const int n, + const float* xyz, + const int m, + const float* xyz2, + float* result, + int* result_i, + float* result2, + int* result2_i) +{ + ChamferDistanceKernel<<>>(b, n, xyz, m, xyz2, result, result_i); + ChamferDistanceKernel<<>>(b, m, xyz2, n, xyz, result2, result2_i); + + cudaError_t err = cudaGetLastError(); + if (err != cudaSuccess) + printf("error in chamfer distance updateOutput: %s\n", cudaGetErrorString(err)); +} + + +__global__ +void ChamferDistanceGradKernel( + int b, int n, + const float* xyz1, + int m, + const float* xyz2, + const float* grad_dist1, + const int* idx1, + float* grad_xyz1, + float* grad_xyz2) +{ + for (int i = blockIdx.x; i>>(b, n, xyz1, m, xyz2, grad_dist1, idx1, grad_xyz1, grad_xyz2); + ChamferDistanceGradKernel<<>>(b, m, xyz2, n, xyz1, grad_dist2, idx2, grad_xyz2, grad_xyz1); + + cudaError_t err = cudaGetLastError(); + if (err != cudaSuccess) + printf("error in chamfer distance get grad: %s\n", cudaGetErrorString(err)); +} diff --git a/thirdparty/learning3d/losses/cuda/chamfer_distance/chamfer_distance.py b/thirdparty/learning3d/losses/cuda/chamfer_distance/chamfer_distance.py new file mode 100644 index 0000000000000000000000000000000000000000..73dfdf923ee0e90faa20b31e0803b738aa62c2a5 --- /dev/null +++ b/thirdparty/learning3d/losses/cuda/chamfer_distance/chamfer_distance.py @@ -0,0 +1,66 @@ +import torch +from torch.utils.cpp_extension import load +import os + +script_dir = os.path.dirname(__file__) +sources = [ + os.path.join(script_dir, "chamfer_distance.cpp"), + os.path.join(script_dir, "chamfer_distance.cu"), +] + +cd = load(name="cd", sources=sources) + + +class ChamferDistanceFunction(torch.autograd.Function): + @staticmethod + def forward(ctx, xyz1, xyz2): + batchsize, n, _ = xyz1.size() + _, m, _ = xyz2.size() + xyz1 = xyz1.contiguous() + xyz2 = xyz2.contiguous() + dist1 = torch.zeros(batchsize, n) + dist2 = torch.zeros(batchsize, m) + + idx1 = torch.zeros(batchsize, n, dtype=torch.int) + idx2 = torch.zeros(batchsize, m, dtype=torch.int) + + if not xyz1.is_cuda: + cd.forward(xyz1, xyz2, dist1, dist2, idx1, idx2) + else: + dist1 = dist1.cuda() + dist2 = dist2.cuda() + idx1 = idx1.cuda() + idx2 = idx2.cuda() + cd.forward_cuda(xyz1, xyz2, dist1, dist2, idx1, idx2) + + ctx.save_for_backward(xyz1, xyz2, idx1, idx2) + + return dist1, dist2 + + @staticmethod + def backward(ctx, graddist1, graddist2): + xyz1, xyz2, idx1, idx2 = ctx.saved_tensors + + graddist1 = graddist1.contiguous() + graddist2 = graddist2.contiguous() + + gradxyz1 = torch.zeros(xyz1.size()) + gradxyz2 = torch.zeros(xyz2.size()) + + if not graddist1.is_cuda: + cd.backward( + xyz1, xyz2, gradxyz1, gradxyz2, graddist1, graddist2, idx1, idx2 + ) + else: + gradxyz1 = gradxyz1.cuda() + gradxyz2 = gradxyz2.cuda() + cd.backward_cuda( + xyz1, xyz2, gradxyz1, gradxyz2, graddist1, graddist2, idx1, idx2 + ) + + return gradxyz1, gradxyz2 + + +class ChamferDistance(torch.nn.Module): + def forward(self, xyz1, xyz2): + return ChamferDistanceFunction.apply(xyz1, xyz2) diff --git a/thirdparty/learning3d/losses/cuda/emd_torch/pkg/emd_loss_layer.py b/thirdparty/learning3d/losses/cuda/emd_torch/pkg/emd_loss_layer.py new file mode 100644 index 0000000000000000000000000000000000000000..6291417b36d9d3b92ed2f898b241a51d760cc4b4 --- /dev/null +++ b/thirdparty/learning3d/losses/cuda/emd_torch/pkg/emd_loss_layer.py @@ -0,0 +1,41 @@ +import torch +import torch.nn as nn + +import _emd_ext._emd as emd + + +class EMDFunction(torch.autograd.Function): + @staticmethod + def forward(self, xyz1, xyz2): + cost, match = emd.emd_forward(xyz1, xyz2) + self.save_for_backward(xyz1, xyz2, match) + return cost + + + @staticmethod + def backward(self, grad_output): + xyz1, xyz2, match = self.saved_tensors + grad_xyz1, grad_xyz2 = emd.emd_backward(xyz1, xyz2, match) + return grad_xyz1, grad_xyz2 + + + + +class EMDLoss(nn.Module): + ''' + Computes the (approximate) Earth Mover's Distance between two point sets. + + IMPLEMENTATION LIMITATIONS: + - Double tensors must have <=11 dimensions + - Float tensors must have <=23 dimensions + This is due to the use of CUDA shared memory in the computation. This shared memory is limited by the hardware to 48kB. + ''' + + def __init__(self): + super(EMDLoss, self).__init__() + + def forward(self, xyz1, xyz2): + + assert xyz1.shape[-1] == xyz2.shape[-1], 'Both point sets must have the same dimensions!' + assert xyz1.shape[1] == xyz2.shape[1], 'Both Point Clouds must have same number of points in it.' + return EMDFunction.apply(xyz1, xyz2) \ No newline at end of file diff --git a/thirdparty/learning3d/losses/cuda/emd_torch/pkg/include/cuda/emd.cuh b/thirdparty/learning3d/losses/cuda/emd_torch/pkg/include/cuda/emd.cuh new file mode 100644 index 0000000000000000000000000000000000000000..546c5b31969165c6a9c2ee799930e2e405e12fe5 --- /dev/null +++ b/thirdparty/learning3d/losses/cuda/emd_torch/pkg/include/cuda/emd.cuh @@ -0,0 +1,347 @@ +#ifndef EMD_CUH_ +#define EMD_CUH_ + +#include "cuda_helper.h" + +template +__global__ void approxmatch(const int b, const int n, const int m, const T * __restrict__ xyz1, const T * __restrict__ xyz2, T * __restrict__ match, T * temp){ + T * remainL=temp+blockIdx.x*(n+m)*2, * remainR=temp+blockIdx.x*(n+m)*2+n,*ratioL=temp+blockIdx.x*(n+m)*2+n+m,*ratioR=temp+blockIdx.x*(n+m)*2+n+m+n; + T multiL,multiR; + if (n>=m){ + multiL=1; + multiR=n/m; + }else{ + multiL=m/n; + multiR=1; + } + const int Block=1024; + __shared__ T buf[Block*4]; + for (int i=blockIdx.x;i=-2;j--){ + T level=-powf(4.0f,j); + if (j==-2){ + level=0; + } + for (int k0=0;k0>>( + b, n, m, + xyz1.data(), + xyz2.data(), + match.data(), + temp.data()); + })); + cudaDeviceSynchronize(); + CUDA_CHECK(cudaGetLastError()) +} + +template +__global__ void matchcost(const int b, const int n, const int m, const T * __restrict__ xyz1, const T * __restrict__ xyz2, const T * __restrict__ match, T * __restrict__ out){ + __shared__ T allsum[512]; + const int Block=1024; + __shared__ T buf[Block*3]; + for (int i=blockIdx.x;i>>( + b, n, m, + xyz1.data(), + xyz2.data(), + match.data(), + out.data()); + })); + CUDA_CHECK(cudaGetLastError()) +} + +template +__global__ void matchcostgrad2(const int b, const int n, const int m,const T * __restrict__ xyz1, const T * __restrict__ xyz2, const T * __restrict__ match, T * __restrict__ grad2){ + __shared__ T sum_grad[256*3]; + for (int i=blockIdx.x;i +__global__ void matchcostgrad1(const int b, const int n, const int m, const T * __restrict__ xyz1, const T * __restrict__ xyz2, const T * __restrict__ match, T * __restrict__ grad1){ + for (int i=blockIdx.x;i>>( + b, n, m, + xyz1.data(), + xyz2.data(), + match.data(), + grad1.data()); + })); + CUDA_CHECK(cudaGetLastError()) + + AT_DISPATCH_FLOATING_TYPES(xyz1.type(), "matchcostgrad2", ([&] { + matchcostgrad2<<>>( + b, n, m, + xyz1.data(), + xyz2.data(), + match.data(), + grad2.data()); + })); + CUDA_CHECK(cudaGetLastError()) +} + +#endif \ No newline at end of file diff --git a/thirdparty/learning3d/losses/cuda/emd_torch/pkg/include/cuda_helper.h b/thirdparty/learning3d/losses/cuda/emd_torch/pkg/include/cuda_helper.h new file mode 100644 index 0000000000000000000000000000000000000000..cba68750dbc1c6fb69780c60b0f0769b2695f9c7 --- /dev/null +++ b/thirdparty/learning3d/losses/cuda/emd_torch/pkg/include/cuda_helper.h @@ -0,0 +1,18 @@ +#ifndef CUDA_HELPER_H_ +#define CUDA_HELPER_H_ + +#define CUDA_CHECK(err) \ + if (cudaSuccess != err) \ + { \ + fprintf(stderr, "CUDA kernel failed: %s (%s:%d)\n", \ + cudaGetErrorString(err), __FILE__, __LINE__); \ + std::exit(-1); \ + } + +#define CHECK_CUDA(x) AT_CHECK(x.type().is_cuda(), \ + #x " must be a CUDA tensor") +#define CHECK_CONTIGUOUS(x) AT_CHECK(x.is_contiguous(), \ + #x " must be contiguous") +#define CHECK_INPUT(x) CHECK_CUDA(x); CHECK_CONTIGUOUS(x) + +#endif \ No newline at end of file diff --git a/thirdparty/learning3d/losses/cuda/emd_torch/pkg/include/emd.h b/thirdparty/learning3d/losses/cuda/emd_torch/pkg/include/emd.h new file mode 100644 index 0000000000000000000000000000000000000000..7e82ed378beb0183a43d48dfc4af1f544f81d908 --- /dev/null +++ b/thirdparty/learning3d/losses/cuda/emd_torch/pkg/include/emd.h @@ -0,0 +1,54 @@ +#ifndef EMD_H_ +#define EMD_H_ + +#include +#include + +#include "cuda_helper.h" + + +std::vector emd_forward_cuda( + at::Tensor xyz1, + at::Tensor xyz2); + +std::vector emd_backward_cuda( + at::Tensor xyz1, + at::Tensor xyz2, + at::Tensor match); + +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +// CALL FUNCTION IMPLEMENTATIONS +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + +std::vector emd_forward( + at::Tensor xyz1, + at::Tensor xyz2) +{ + CHECK_INPUT(xyz1); + CHECK_INPUT(xyz2); + + return emd_forward_cuda(xyz1, xyz2); +} + +std::vector emd_backward( + at::Tensor xyz1, + at::Tensor xyz2, + at::Tensor match) +{ + CHECK_INPUT(xyz1); + CHECK_INPUT(xyz2); + CHECK_INPUT(match); + + return emd_backward_cuda(xyz1, xyz2, match); +} + +PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) { + m.def("emd_forward", &emd_forward, "Compute Earth Mover's Distance"); + m.def("emd_backward", &emd_backward, "Compute Gradients for Earth Mover's Distance"); +} + + + +#endif \ No newline at end of file diff --git a/thirdparty/learning3d/losses/cuda/emd_torch/pkg/layer/__init__.py b/thirdparty/learning3d/losses/cuda/emd_torch/pkg/layer/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d336b6326a4d6669749e8e7bc9175991bdd4dbfd --- /dev/null +++ b/thirdparty/learning3d/losses/cuda/emd_torch/pkg/layer/__init__.py @@ -0,0 +1 @@ +from .emd_loss_layer import EMDLoss \ No newline at end of file diff --git a/thirdparty/learning3d/losses/cuda/emd_torch/pkg/layer/emd_loss_layer.py b/thirdparty/learning3d/losses/cuda/emd_torch/pkg/layer/emd_loss_layer.py new file mode 100644 index 0000000000000000000000000000000000000000..d240ac4efd02f86ad4dabe9ab83f87b4044df604 --- /dev/null +++ b/thirdparty/learning3d/losses/cuda/emd_torch/pkg/layer/emd_loss_layer.py @@ -0,0 +1,40 @@ +import torch +import torch.nn as nn + +import _emd_ext._emd as emd + + +class EMDFunction(torch.autograd.Function): + @staticmethod + def forward(self, xyz1, xyz2): + cost, match = emd.emd_forward(xyz1, xyz2) + self.save_for_backward(xyz1, xyz2, match) + return cost + + + @staticmethod + def backward(self, grad_output): + xyz1, xyz2, match = self.saved_tensors + grad_xyz1, grad_xyz2 = emd.emd_backward(xyz1, xyz2, match) + return grad_xyz1, grad_xyz2 + + + + +class EMDLoss(nn.Module): + ''' + Computes the (approximate) Earth Mover's Distance between two point sets. + + IMPLEMENTATION LIMITATIONS: + - Double tensors must have <=11 dimensions + - Float tensors must have <=23 dimensions + This is due to the use of CUDA shared memory in the computation. This shared memory is limited by the hardware to 48kB. + ''' + + def __init__(self): + super(EMDLoss, self).__init__() + + def forward(self, xyz1, xyz2): + + assert xyz1.shape[-1] == xyz2.shape[-1], 'Both point sets must have the same dimensionality' + return EMDFunction.apply(xyz1, xyz2) \ No newline at end of file diff --git a/thirdparty/learning3d/losses/cuda/emd_torch/setup.py b/thirdparty/learning3d/losses/cuda/emd_torch/setup.py new file mode 100644 index 0000000000000000000000000000000000000000..ce95e1c4e4e7f0e75e30a3e669c84c4798db5b36 --- /dev/null +++ b/thirdparty/learning3d/losses/cuda/emd_torch/setup.py @@ -0,0 +1,29 @@ +from setuptools import setup +from torch.utils.cpp_extension import BuildExtension, CUDAExtension + + +setup( + name='PyTorch EMD', + version='0.0', + author='Vinit Sarode', + author_email='vinitsarode5@gmail.com', + description='A PyTorch module for the earth mover\'s distance loss', + ext_package='_emd_ext', + ext_modules=[ + CUDAExtension( + name='_emd', + sources=[ + 'pkg/src/emd.cpp', + 'pkg/src/cuda/emd.cu', + ], + include_dirs=['pkg/include'], + ), + ], + packages=[ + 'emd', + ], + package_dir={ + 'emd' : 'pkg/layer' + }, + cmdclass={'build_ext': BuildExtension}, +) \ No newline at end of file diff --git a/thirdparty/learning3d/losses/emd.py b/thirdparty/learning3d/losses/emd.py new file mode 100644 index 0000000000000000000000000000000000000000..112af476671f8f3abf12c05db08963116a94ceaf --- /dev/null +++ b/thirdparty/learning3d/losses/emd.py @@ -0,0 +1,16 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F + +def emd(template: torch.Tensor, source: torch.Tensor): + from emd import EMDLoss + emd_loss = torch.mean(self.emd(template, source))/(template.size()[1]) + return emd_loss + + +class EMDLoss(nn.Module): + def __init__(self): + super(EMDLoss, self).__init__() + + def forward(self, template, source): + return emd(template, source) \ No newline at end of file diff --git a/thirdparty/learning3d/losses/frobenius_norm.py b/thirdparty/learning3d/losses/frobenius_norm.py new file mode 100644 index 0000000000000000000000000000000000000000..852f2d18da3cf44cc6a0671203647d91fc0b7693 --- /dev/null +++ b/thirdparty/learning3d/losses/frobenius_norm.py @@ -0,0 +1,21 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F + +def frobeniusNormLoss(predicted, igt): + """ |predicted*igt - I| (should be 0) """ + assert predicted.size(0) == igt.size(0) + assert predicted.size(1) == igt.size(1) and predicted.size(1) == 4 + assert predicted.size(2) == igt.size(2) and predicted.size(2) == 4 + + error = predicted.matmul(igt) + I = torch.eye(4).to(error).view(1, 4, 4).expand(error.size(0), 4, 4) + return torch.nn.functional.mse_loss(error, I, size_average=True) * 16 + + +class FrobeniusNormLoss(nn.Module): + def __init__(self): + super(FrobeniusNormLoss, self).__init__() + + def forward(self, predicted, igt): + return frobeniusNormLoss(predicted, igt) \ No newline at end of file diff --git a/thirdparty/learning3d/losses/rmse_features.py b/thirdparty/learning3d/losses/rmse_features.py new file mode 100644 index 0000000000000000000000000000000000000000..d11d195583667c1159c6e8d551c169aa9c047bdc --- /dev/null +++ b/thirdparty/learning3d/losses/rmse_features.py @@ -0,0 +1,16 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F + +def rmseOnFeatures(feature_difference): + # |feature_difference| should be 0 + gt = torch.zeros_like(feature_difference) + return torch.nn.functional.mse_loss(feature_difference, gt, size_average=False) + + +class RMSEFeaturesLoss(nn.Module): + def __init__(self): + super(RMSEFeaturesLoss, self).__init__() + + def forward(self, feature_difference): + return rmseOnFeatures(feature_difference) \ No newline at end of file diff --git a/thirdparty/learning3d/models/__init__.py b/thirdparty/learning3d/models/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8ecc140ceb686afeb7057e649e0894207a2d64bb --- /dev/null +++ b/thirdparty/learning3d/models/__init__.py @@ -0,0 +1,24 @@ +from .pointnet import PointNet +from .pointconv import create_pointconv +from .dgcnn import DGCNN +from .ppfnet import PPFNet +from .pooling import Pooling + +from .classifier import Classifier +from .segmentation import Segmentation + +from .dcp import DCP +from .prnet import PRNet +from .pcrnet import iPCRNet +from .pointnetlk import PointNetLK +from .rpmnet import RPMNet +from .pcn import PCN +from .deepgmr import DeepGMR +from .masknet import MaskNet +from .masknet2 import MaskNet2 +from .curvenet import CurveNet + +try: + from .flownet3d import FlowNet3D +except: + print("Error raised in pointnet2 module for FlowNet3D Network!\nEither don't use pointnet2_utils or retry it's setup.") \ No newline at end of file diff --git a/thirdparty/learning3d/models/classifier.py b/thirdparty/learning3d/models/classifier.py new file mode 100644 index 0000000000000000000000000000000000000000..ec2f9c414ae5becf805122109444ad65630a91b2 --- /dev/null +++ b/thirdparty/learning3d/models/classifier.py @@ -0,0 +1,41 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F +from .pooling import Pooling + +class Classifier(nn.Module): + def __init__(self, feature_model, num_classes=40): + super(Classifier, self).__init__() + self.feature_model = feature_model + self.num_classes = num_classes + + self.linear1 = torch.nn.Linear(self.feature_model.emb_dims, 512) + self.bn1 = torch.nn.BatchNorm1d(512) + self.dropout1 = torch.nn.Dropout(p=0.7) + self.linear2 = torch.nn.Linear(512, 256) + self.bn2 = torch.nn.BatchNorm1d(256) + self.dropout2 = torch.nn.Dropout(p=0.7) + self.linear3 = torch.nn.Linear(256, self.num_classes) + + self.pooling = Pooling('max') + + def forward(self, input_data): + output = self.pooling(self.feature_model(input_data)) + output = F.relu(self.bn1(self.linear1(output))) + output = self.dropout1(output) + output = F.relu(self.bn2(self.linear2(output))) + output = self.dropout2(output) + output = self.linear3(output) + return output + + +if __name__ == '__main__': + from pointnet import PointNet + x = torch.rand(10,1024,3) + + pn = PointNet() + classifier = Classifier(pn) + classes = classifier(x) + + print('Input Shape: {}\nClassification Output Shape: {}' + .format(x.shape, classes.shape)) \ No newline at end of file diff --git a/thirdparty/learning3d/models/curvenet.py b/thirdparty/learning3d/models/curvenet.py new file mode 100644 index 0000000000000000000000000000000000000000..fdf0913d231ba63db1f6424aa5412a5421124277 --- /dev/null +++ b/thirdparty/learning3d/models/curvenet.py @@ -0,0 +1,130 @@ +""" +@Author: Tiange Xiang +@Contact: txia7609@uni.sydney.edu.au +@File: curvenet_cls.py +@Time: 2021/01/21 3:10 PM +""" + +import torch +import torch.nn as nn +import torch.nn.functional as F +from .. utils import ( + index_points, + farthest_point_sample, + query_ball_point, + LPFA, + CIC +) + +def sample_and_group(npoint, radius, nsample, xyz, points, returnfps=False): + """ + Input: + npoint: + radius: + nsample: + xyz: input points position data, [B, N, 3] + points: input points data, [B, N, D] + Return: + new_xyz: sampled points position data, [B, npoint, nsample, 3] + new_points: sampled points data, [B, npoint, nsample, 3+D] + """ + new_xyz = index_points(xyz, farthest_point_sample(xyz, npoint)) + torch.cuda.empty_cache() + + idx = query_ball_point(radius, nsample, xyz, new_xyz) + torch.cuda.empty_cache() + + new_points = index_points(points, idx) + torch.cuda.empty_cache() + + if returnfps: + return new_xyz, new_points, idx + else: + return new_xyz, new_points + +curve_config = { + 'default': [[100, 5], [100, 5], None, None], + 'long': [[10, 30], None, None, None] + } + +class CurveNet(nn.Module): + def __init__(self, num_classes=40, k=20, setting='default', input_shape="bnc", emb_dims=2048, classifier=True): + super(CurveNet, self).__init__() + + if input_shape not in ["bcn", "bnc"]: + raise ValueError("Allowed shapes are 'bcn' (batch * channels * num_in_points), 'bnc' ") + + self.input_shape = input_shape + + assert setting in curve_config + + additional_channel = 32 + self.classifier = classifier + self.lpfa = LPFA(9, additional_channel, k=k, mlp_num=1, initial=True) + + # encoder + self.cic11 = CIC(npoint=1024, radius=0.05, k=k, in_channels=additional_channel, output_channels=64, bottleneck_ratio=2, mlp_num=1, curve_config=curve_config[setting][0]) + self.cic12 = CIC(npoint=1024, radius=0.05, k=k, in_channels=64, output_channels=64, bottleneck_ratio=4, mlp_num=1, curve_config=curve_config[setting][0]) + + self.cic21 = CIC(npoint=1024, radius=0.05, k=k, in_channels=64, output_channels=128, bottleneck_ratio=2, mlp_num=1, curve_config=curve_config[setting][1]) + self.cic22 = CIC(npoint=1024, radius=0.1, k=k, in_channels=128, output_channels=128, bottleneck_ratio=4, mlp_num=1, curve_config=curve_config[setting][1]) + + self.cic31 = CIC(npoint=256, radius=0.1, k=k, in_channels=128, output_channels=256, bottleneck_ratio=2, mlp_num=1, curve_config=curve_config[setting][2]) + self.cic32 = CIC(npoint=256, radius=0.2, k=k, in_channels=256, output_channels=256, bottleneck_ratio=4, mlp_num=1, curve_config=curve_config[setting][2]) + + self.cic41 = CIC(npoint=64, radius=0.2, k=k, in_channels=256, output_channels=512, bottleneck_ratio=2, mlp_num=1, curve_config=curve_config[setting][3]) + self.cic42 = CIC(npoint=64, radius=0.4, k=k, in_channels=512, output_channels=512, bottleneck_ratio=4, mlp_num=1, curve_config=curve_config[setting][3]) + + self.conv0 = nn.Sequential( + nn.Conv1d(512, emb_dims//2, kernel_size=1, bias=False), + nn.BatchNorm1d(emb_dims//2), + nn.ReLU(inplace=True)) + + if self.classifier: + self.conv1 = nn.Linear(emb_dims, 512, bias=False) + self.conv2 = nn.Linear(512, num_classes) + self.bn1 = nn.BatchNorm1d(512) + self.dp1 = nn.Dropout(p=0.5) + + def forward(self, xyz, get_flatten_curve_idxs=False): + flatten_curve_idxs = {} + if self.input_shape == 'bnc': + xyz = xyz.permute(0, 2, 1) + + l0_points = self.lpfa(xyz, xyz) + + l1_xyz, l1_points, flatten_curve_idxs_11 = self.cic11(xyz, l0_points) + flatten_curve_idxs['flatten_curve_idxs_11'] = flatten_curve_idxs_11 + l1_xyz, l1_points, flatten_curve_idxs_12 = self.cic12(l1_xyz, l1_points) + flatten_curve_idxs['flatten_curve_idxs_12'] = flatten_curve_idxs_12 + + l2_xyz, l2_points, flatten_curve_idxs_21 = self.cic21(l1_xyz, l1_points) + flatten_curve_idxs['flatten_curve_idxs_21'] = flatten_curve_idxs_21 + l2_xyz, l2_points, flatten_curve_idxs_22 = self.cic22(l2_xyz, l2_points) + flatten_curve_idxs['flatten_curve_idxs_22'] = flatten_curve_idxs_22 + + l3_xyz, l3_points, flatten_curve_idxs_31 = self.cic31(l2_xyz, l2_points) + flatten_curve_idxs['flatten_curve_idxs_31'] = flatten_curve_idxs_31 + l3_xyz, l3_points, flatten_curve_idxs_32 = self.cic32(l3_xyz, l3_points) + flatten_curve_idxs['flatten_curve_idxs_32'] = flatten_curve_idxs_32 + + l4_xyz, l4_points, flatten_curve_idxs_41 = self.cic41(l3_xyz, l3_points) + flatten_curve_idxs['flatten_curve_idxs_41'] = flatten_curve_idxs_41 + l4_xyz, l4_points, flatten_curve_idxs_42 = self.cic42(l4_xyz, l4_points) + flatten_curve_idxs['flatten_curve_idxs_42'] = flatten_curve_idxs_42 + + x = self.conv0(l4_points) + x_max = F.adaptive_max_pool1d(x, 1) + x_avg = F.adaptive_avg_pool1d(x, 1) + + x = torch.cat((x_max, x_avg), dim=1).squeeze(-1) + + if self.classifier: + x = F.relu(self.bn1(self.conv1(x).unsqueeze(-1)), inplace=True).squeeze(-1) + x = self.dp1(x) + x = self.conv2(x) + + if get_flatten_curve_idxs: + return x, flatten_curve_idxs + else: + return x diff --git a/thirdparty/learning3d/models/dcp.py b/thirdparty/learning3d/models/dcp.py new file mode 100644 index 0000000000000000000000000000000000000000..f0cda46d7201447fe8ab12106543efa1e48e3716 --- /dev/null +++ b/thirdparty/learning3d/models/dcp.py @@ -0,0 +1,92 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F +from .dgcnn import DGCNN +from .pointnet import PointNet +from .. ops import transform_functions as transform +from .. utils import Transformer, SVDHead, Identity + + +class DCP(nn.Module): + def __init__(self, feature_model=DGCNN(), cycle=False, pointer_='transformer', head='svd'): + super(DCP, self).__init__() + self.cycle = cycle + self.emb_nn = feature_model + + if pointer_ == 'identity': + self.pointer = Identity() + elif pointer_ == 'transformer': + self.pointer = Transformer(self.emb_nn.emb_dims, n_blocks=1, dropout=0.0, ff_dims=1024, n_heads=4) + else: + raise Exception("Not implemented") + + if head == 'mlp': + self.head = MLPHead(self.emb_nn.emb_dims) + elif head == 'svd': + self.head = SVDHead(self.emb_nn.emb_dims) + else: + raise Exception('Not implemented') + + def forward(self, template, source): + source_features = self.emb_nn(source) + template_features = self.emb_nn(template) + + source_features_p, template_features_p = self.pointer(source_features, template_features) + + source_features = source_features + source_features_p + template_features = template_features + template_features_p + + rotation_ab, translation_ab = self.head(source_features, template_features, source, template) + if self.cycle: + rotation_ba, translation_ba = self.head(template_features, source_features, template, source) + else: + rotation_ba = rotation_ab.transpose(2, 1).contiguous() + translation_ba = -torch.matmul(rotation_ba, translation_ab.unsqueeze(2)).squeeze(2) + + transformed_source = transform.transform_point_cloud(source, rotation_ab, translation_ab) + + result = {'est_R': rotation_ab, + 'est_t': translation_ab, + 'est_R_': rotation_ba, + 'est_t_': translation_ba, + 'est_T': transform.convert2transformation(rotation_ab, translation_ab), + 'r': template_features - source_features, + 'transformed_source': transformed_source} + return result + + +class MLPHead(nn.Module): + def __init__(self, emb_dims): + super(MLPHead, self).__init__() + self.emb_dims = emb_dims + self.nn = nn.Sequential(nn.Linear(emb_dims * 2, emb_dims // 2), + nn.BatchNorm1d(emb_dims // 2), + nn.ReLU(), + nn.Linear(emb_dims // 2, emb_dims // 4), + nn.BatchNorm1d(emb_dims // 4), + nn.ReLU(), + nn.Linear(emb_dims // 4, emb_dims // 8), + nn.BatchNorm1d(emb_dims // 8), + nn.ReLU()) + self.proj_rot = nn.Linear(emb_dims // 8, 4) + self.proj_trans = nn.Linear(emb_dims // 8, 3) + + def forward(self, *input): + src_embedding = input[0] + tgt_embedding = input[1] + embedding = torch.cat((src_embedding, tgt_embedding), dim=1) + embedding = self.nn(embedding.max(dim=-1)[0]) + rotation = self.proj_rot(embedding) + rotation = rotation / torch.norm(rotation, p=2, dim=1, keepdim=True) + translation = self.proj_trans(embedding) + return quat2mat(rotation), translation + + +if __name__ == '__main__': + template, source = torch.rand(10,1024,3), torch.rand(10,1024,3) + pn = PointNet() + + # Not Tested Yet. + net = DCP(pn) + result = net(template, source) + import ipdb; ipdb.set_trace() \ No newline at end of file diff --git a/thirdparty/learning3d/models/deepgmr.py b/thirdparty/learning3d/models/deepgmr.py new file mode 100644 index 0000000000000000000000000000000000000000..6fd5db8cea224ec4103b2b733725ee5141846324 --- /dev/null +++ b/thirdparty/learning3d/models/deepgmr.py @@ -0,0 +1,165 @@ +''' +We thank the author of DeepGMR paper to open-source their code. +Modified by Vinit Sarode. +''' + +import math +import torch +import torch.nn as nn +import torch.nn.functional as F +from .. ops import transform_functions as transform + + +def gmm_params(gamma, pts): + ''' + Inputs: + gamma: B x N x J + pts: B x N x 3 + ''' + # pi: B x J + pi = gamma.mean(dim=1) + Npi = pi * gamma.shape[1] + # mu: B x J x 3 + mu = gamma.transpose(1, 2) @ pts / Npi.unsqueeze(2) + # diff: B x N x J x 3 + diff = pts.unsqueeze(2) - mu.unsqueeze(1) + # sigma: B x J x 3 x 3 + eye = torch.eye(3).unsqueeze(0).unsqueeze(1).to(gamma.device) + sigma = ( + ((diff.unsqueeze(3) @ diff.unsqueeze(4)).squeeze() * gamma).sum(dim=1) / Npi + ).unsqueeze(2).unsqueeze(3) * eye + return pi, mu, sigma + + +def gmm_register(pi_s, mu_s, mu_t, sigma_t): + ''' + Inputs: + pi: B x J + mu: B x J x 3 + sigma: B x J x 3 x 3 + ''' + c_s = pi_s.unsqueeze(1) @ mu_s + c_t = pi_s.unsqueeze(1) @ mu_t + Ms = torch.sum((pi_s.unsqueeze(2) * (mu_s - c_s)).unsqueeze(3) @ + (mu_t - c_t).unsqueeze(2) @ sigma_t.inverse(), dim=1) + U, _, V = torch.svd(Ms.cpu()) + U = U.cuda() if torch.cuda.is_available() else U + V = V.cuda() if torch.cuda.is_available() else V + S = torch.eye(3).unsqueeze(0).repeat(U.shape[0], 1, 1).to(U.device) + S[:, 2, 2] = torch.det(V @ U.transpose(1, 2)) + R = V @ S @ U.transpose(1, 2) + t = c_t.transpose(1, 2) - R @ c_s.transpose(1, 2) + bot_row = torch.Tensor([[[0, 0, 0, 1]]]).repeat(R.shape[0], 1, 1).to(R.device) + T = torch.cat([torch.cat([R, t], dim=2), bot_row], dim=1) + return T + + +class Conv1dBNReLU(nn.Sequential): + def __init__(self, in_planes, out_planes): + super(Conv1dBNReLU, self).__init__( + nn.Conv1d(in_planes, out_planes, kernel_size=1, bias=False), + nn.BatchNorm1d(out_planes), + nn.ReLU(inplace=True)) + + +class FCBNReLU(nn.Sequential): + def __init__(self, in_planes, out_planes): + super(FCBNReLU, self).__init__( + nn.Linear(in_planes, out_planes, bias=False), + nn.BatchNorm1d(out_planes), + nn.ReLU(inplace=True)) + + +class TNet(nn.Module): + def __init__(self): + super(TNet, self).__init__() + self.encoder = nn.Sequential( + Conv1dBNReLU(3, 64), + Conv1dBNReLU(64, 128), + Conv1dBNReLU(128, 256)) + self.decoder = nn.Sequential( + FCBNReLU(256, 128), + FCBNReLU(128, 64), + nn.Linear(64, 6)) + + @staticmethod + def f2R(f): + r1 = F.normalize(f[:, :3]) + proj = (r1.unsqueeze(1) @ f[:, 3:].unsqueeze(2)).squeeze(2) + r2 = F.normalize(f[:, 3:] - proj * r1) + r3 = r1.cross(r2) + return torch.stack([r1, r2, r3], dim=2) + + def forward(self, pts): + f = self.encoder(pts) + f, _ = f.max(dim=2) + f = self.decoder(f) + R = self.f2R(f) + return R @ pts + + +class PointNet(nn.Module): + def __init__(self, use_rri, use_tnet=False, nearest_neighbors=20): + super(PointNet, self).__init__() + self.use_tnet = use_tnet + self.tnet = TNet() if self.use_tnet else None + d_input = nearest_neighbors * 4 if use_rri else 3 + self.encoder = nn.Sequential( + Conv1dBNReLU(d_input, 64), + Conv1dBNReLU(64, 128), + Conv1dBNReLU(128, 256), + Conv1dBNReLU(256, args.d_model)) + self.decoder = nn.Sequential( + Conv1dBNReLU(args.d_model * 2, 512), + Conv1dBNReLU(512, 256), + Conv1dBNReLU(256, 128), + nn.Conv1d(128, args.n_clusters, kernel_size=1)) + + def forward(self, pts): + pts = self.tnet(pts) if self.use_tnet else pts + f_loc = self.encoder(pts) + f_glob, _ = f_loc.max(dim=2) + f_glob = f_glob.unsqueeze(2).expand_as(f_loc) + y = self.decoder(torch.cat([f_loc, f_glob], dim=1)) + return y.transpose(1, 2) + + +class DeepGMR(nn.Module): + def __init__(self, use_rri=True, feature_model=None, nearest_neighbors=20): + super(DeepGMR, self).__init__() + self.backbone = feature_model if not None else PointNet(use_rri=use_rri, nearest_neighbors=nearest_neighbors) + self.use_rri = use_rri + + def forward(self, template, source): + if self.use_rri: + self.template = template[..., :3] + self.source = source[..., :3] + template_features = template[..., 3:].transpose(1, 2) + source_features = source[..., 3:].transpose(1, 2) + else: + self.template = template + self.source = source + template_features = (template - template.mean(dim=2, keepdim=True)).transpose(1, 2) + source_features = (source - source.mean(dim=2, keepdim=True)).transpose(1, 2) + + self.template_gamma = F.softmax(self.backbone(template_features), dim=2) + self.template_pi, self.template_mu, self.template_sigma = gmm_params(self.template_gamma, self.template) + self.source_gamma = F.softmax(self.backbone(source_features), dim=2) + self.source_pi, self.source_mu, self.source_sigma = gmm_params(self.source_gamma, self.source) + + self.est_T_inverse = gmm_register(self.template_pi, self.template_mu, self.source_mu, self.source_sigma) + self.est_T = gmm_register(self.source_pi, self.source_mu, self.template_mu, self.template_sigma) # [template = source * est_T] + self.igt = igt # [source = template * igt] + + transformed_source = transform.transform_point_cloud(source, est_T[:, :3, :3], est_T[:, :3, 3]) + + result = {'est_R': est_T[:, :3, :3], + 'est_t': est_T[:, :3, 3], + 'est_R_inverse': est_T_inverse[:, :3, :3], + 'est_t_inverese': est_T_inverse[:, :3, 3], + 'est_T': est_T, + 'est_T_inverse': est_T_inverse, + 'r': template_features - source_features, + 'transformed_source': transformed_source} + + return result diff --git a/thirdparty/learning3d/models/dgcnn.py b/thirdparty/learning3d/models/dgcnn.py new file mode 100644 index 0000000000000000000000000000000000000000..6626752a2ca84a9df7c38211de4fa1e19ba1b2ea --- /dev/null +++ b/thirdparty/learning3d/models/dgcnn.py @@ -0,0 +1,58 @@ +import torch +import torch.nn.functional as F +from .. utils import knn, get_graph_feature + + +class DGCNN(torch.nn.Module): + def __init__(self, emb_dims=1024, input_shape="bnc"): + super(DGCNN, self).__init__() + if input_shape not in ["bcn", "bnc"]: + raise ValueError("Allowed shapes are 'bcn' (batch * channels * num_in_points), 'bnc' ") + self.input_shape = input_shape + self.emb_dims = emb_dims + + self.conv1 = torch.nn.Conv2d(6, 64, kernel_size=1, bias=False) + self.conv2 = torch.nn.Conv2d(64, 64, kernel_size=1, bias=False) + self.conv3 = torch.nn.Conv2d(64, 128, kernel_size=1, bias=False) + self.conv4 = torch.nn.Conv2d(128, 256, kernel_size=1, bias=False) + self.conv5 = torch.nn.Conv2d(512, emb_dims, kernel_size=1, bias=False) + self.bn1 = torch.nn.BatchNorm2d(64) + self.bn2 = torch.nn.BatchNorm2d(64) + self.bn3 = torch.nn.BatchNorm2d(128) + self.bn4 = torch.nn.BatchNorm2d(256) + self.bn5 = torch.nn.BatchNorm2d(emb_dims) + + def forward(self, input_data): + if self.input_shape == "bnc": + input_data = input_data.permute(0, 2, 1) + if input_data.shape[1] != 3: + raise RuntimeError("shape of x must be of [Batch x 3 x NumInPoints]") + + batch_size, num_dims, num_points = input_data.size() + output = get_graph_feature(input_data) + + output = F.relu(self.bn1(self.conv1(output))) + output1 = output.max(dim=-1, keepdim=True)[0] + + output = F.relu(self.bn2(self.conv2(output))) + output2 = output.max(dim=-1, keepdim=True)[0] + + output = F.relu(self.bn3(self.conv3(output))) + output3 = output.max(dim=-1, keepdim=True)[0] + + output = F.relu(self.bn4(self.conv4(output))) + output4 = output.max(dim=-1, keepdim=True)[0] + + output = torch.cat((output1, output2, output3, output4), dim=1) + + output = F.relu(self.bn5(self.conv5(output))).view(batch_size, -1, num_points) + return output + + +if __name__ == '__main__': + # Test the code. + x = torch.rand((10,1024,3)) + + dgcnn = DGCNN() + y = dgcnn(x) + print("\nInput Shape of DGCNN: ", x.shape, "\nOutput Shape of DGCNN: ", y.shape) \ No newline at end of file diff --git a/thirdparty/learning3d/models/flownet3d.py b/thirdparty/learning3d/models/flownet3d.py new file mode 100644 index 0000000000000000000000000000000000000000..531833eff4593af96e3348298bd1be579ef8dda7 --- /dev/null +++ b/thirdparty/learning3d/models/flownet3d.py @@ -0,0 +1,338 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F +from time import time +import numpy as np +from .. utils import ( + pc_normalize, + square_distance, + index_points, + farthest_point_sample, + knn_point, + query_ball_point +) + +try: + from .. utils import pointnet2_utils as pointutils +except: + print("Error in pointnet2_utils! Retry setup for pointnet2_utils.") + +def timeit(tag, t): + print("{}: {}s".format(tag, time() - t)) + return time() + +def sample_and_group(npoint, radius, nsample, xyz, points, returnfps=False): + """ + Input: + npoint: + radius: + nsample: + xyz: input points position data, [B, N, C] + points: input points data, [B, N, D] + Return: + new_xyz: sampled points position data, [B, 1, C] + new_points: sampled points data, [B, 1, N, C+D] + """ + B, N, C = xyz.shape + S = npoint + fps_idx = farthest_point_sample(xyz, npoint) # [B, npoint, C] + new_xyz = index_points(xyz, fps_idx) + idx = query_ball_point(radius, nsample, xyz, new_xyz, get_cnt=False) + grouped_xyz = index_points(xyz, idx) # [B, npoint, nsample, C] + grouped_xyz_norm = grouped_xyz - new_xyz.view(B, S, 1, C) + if points is not None: + grouped_points = index_points(points, idx) + new_points = torch.cat([grouped_xyz_norm, grouped_points], dim=-1) # [B, npoint, nsample, C+D] + else: + new_points = grouped_xyz_norm + if returnfps: + return new_xyz, new_points, grouped_xyz, fps_idx + else: + return new_xyz, new_points + +def sample_and_group_all(xyz, points): + """ + Input: + xyz: input points position data, [B, N, C] + points: input points data, [B, N, D] + Return: + new_xyz: sampled points position data, [B, 1, C] + new_points: sampled points data, [B, 1, N, C+D] + """ + device = xyz.device + B, N, C = xyz.shape + new_xyz = torch.zeros(B, 1, C).to(device) + grouped_xyz = xyz.view(B, 1, N, C) + if points is not None: + new_points = torch.cat([grouped_xyz, points.view(B, 1, N, -1)], dim=-1) + else: + new_points = grouped_xyz + return new_xyz, new_points + + +class PointNetSetAbstraction(nn.Module): + def __init__(self, npoint, radius, nsample, in_channel, mlp, group_all): + super(PointNetSetAbstraction, self).__init__() + self.npoint = npoint + self.radius = radius + self.nsample = nsample + self.group_all = group_all + self.mlp_convs = nn.ModuleList() + self.mlp_bns = nn.ModuleList() + last_channel = in_channel+3 # TODO: + for out_channel in mlp: + self.mlp_convs.append(nn.Conv2d(last_channel, out_channel, 1, bias = False)) + self.mlp_bns.append(nn.BatchNorm2d(out_channel)) + last_channel = out_channel + + if group_all: + self.queryandgroup = pointutils.GroupAll() + else: + self.queryandgroup = pointutils.QueryAndGroup(radius, nsample) + + def forward(self, xyz, points): + """ + Input: + xyz: input points position data, [B, C, N] + points: input points data, [B, D, N] + Return: + new_xyz: sampled points position data, [B, S, C] + new_points_concat: sample points feature data, [B, S, D'] + """ + device = xyz.device + B, C, N = xyz.shape + xyz_t = xyz.permute(0, 2, 1).contiguous() + # if points is not None: + # points = points.permute(0, 2, 1).contiguous() + + # 选取邻域点 + if self.group_all == False: + fps_idx = pointutils.furthest_point_sample(xyz_t, self.npoint) # [B, N] + new_xyz = pointutils.gather_operation(xyz, fps_idx) # [B, C, N] + else: + new_xyz = xyz + new_points = self.queryandgroup(xyz_t, new_xyz.transpose(2, 1).contiguous(), points) # [B, 3+C, N, S] + + # new_xyz: sampled points position data, [B, C, npoint] + # new_points: sampled points data, [B, C+D, npoint, nsample] + for i, conv in enumerate(self.mlp_convs): + bn = self.mlp_bns[i] + new_points = F.relu(bn(conv(new_points))) + + new_points = torch.max(new_points, -1)[0] + return new_xyz, new_points + +class FlowEmbedding(nn.Module): + def __init__(self, radius, nsample, in_channel, mlp, pooling='max', corr_func='concat', knn = True): + super(FlowEmbedding, self).__init__() + self.radius = radius + self.nsample = nsample + self.knn = knn + self.pooling = pooling + self.corr_func = corr_func + self.mlp_convs = nn.ModuleList() + self.mlp_bns = nn.ModuleList() + if corr_func is 'concat': + last_channel = in_channel*2+3 + for out_channel in mlp: + self.mlp_convs.append(nn.Conv2d(last_channel, out_channel, 1, bias=False)) + self.mlp_bns.append(nn.BatchNorm2d(out_channel)) + last_channel = out_channel + + def forward(self, pos1, pos2, feature1, feature2): + """ + Input: + xyz1: (batch_size, 3, npoint) + xyz2: (batch_size, 3, npoint) + feat1: (batch_size, channel, npoint) + feat2: (batch_size, channel, npoint) + Output: + xyz1: (batch_size, 3, npoint) + feat1_new: (batch_size, mlp[-1], npoint) + """ + pos1_t = pos1.permute(0, 2, 1).contiguous() + pos2_t = pos2.permute(0, 2, 1).contiguous() + B, N, C = pos1_t.shape + if self.knn: + _, idx = pointutils.knn(self.nsample, pos1_t, pos2_t) + else: + # If the ball neighborhood points are less than nsample, + # than use the knn neighborhood points + idx, cnt = query_ball_point(self.radius, self.nsample, pos2_t, pos1_t, get_cnt=True) + # 利用knn取最近的那些点 + _, idx_knn = pointutils.knn(self.nsample, pos1_t, pos2_t) + cnt = cnt.view(B, -1, 1).repeat(1, 1, self.nsample) + idx = idx_knn[cnt > (self.nsample-1)] + + pos2_grouped = pointutils.grouping_operation(pos2, idx) # [B, 3, N, S] + pos_diff = pos2_grouped - pos1.view(B, -1, N, 1) # [B, 3, N, S] + + feat2_grouped = pointutils.grouping_operation(feature2, idx) # [B, C, N, S] + if self.corr_func=='concat': + feat_diff = torch.cat([feat2_grouped, feature1.view(B, -1, N, 1).repeat(1, 1, 1, self.nsample)], dim = 1) + + feat1_new = torch.cat([pos_diff, feat_diff], dim = 1) # [B, 2*C+3,N,S] + for i, conv in enumerate(self.mlp_convs): + bn = self.mlp_bns[i] + feat1_new = F.relu(bn(conv(feat1_new))) + + feat1_new = torch.max(feat1_new, -1)[0] # [B, mlp[-1], npoint] + return pos1, feat1_new + +class PointNetSetUpConv(nn.Module): + def __init__(self, nsample, radius, f1_channel, f2_channel, mlp, mlp2, knn = True): + super(PointNetSetUpConv, self).__init__() + self.nsample = nsample + self.radius = radius + self.knn = knn + self.mlp1_convs = nn.ModuleList() + self.mlp2_convs = nn.ModuleList() + last_channel = f2_channel+3 + for out_channel in mlp: + self.mlp1_convs.append(nn.Sequential(nn.Conv2d(last_channel, out_channel, 1, bias=False), + nn.BatchNorm2d(out_channel), + nn.ReLU(inplace=False))) + last_channel = out_channel + if len(mlp) is not 0: + last_channel = mlp[-1] + f1_channel + else: + last_channel = last_channel + f1_channel + for out_channel in mlp2: + self.mlp2_convs.append(nn.Sequential(nn.Conv1d(last_channel, out_channel, 1, bias=False), + nn.BatchNorm1d(out_channel), + nn.ReLU(inplace=False))) + last_channel = out_channel + + def forward(self, pos1, pos2, feature1, feature2): + """ + Feature propagation from xyz2 (less points) to xyz1 (more points) + Inputs: + xyz1: (batch_size, 3, npoint1) + xyz2: (batch_size, 3, npoint2) + feat1: (batch_size, channel1, npoint1) features for xyz1 points (earlier layers, more points) + feat2: (batch_size, channel1, npoint2) features for xyz2 points + Output: + feat1_new: (batch_size, npoint2, mlp[-1] or mlp2[-1] or channel1+3) + TODO: Add support for skip links. Study how delta(XYZ) plays a role in feature updating. + """ + pos1_t = pos1.permute(0, 2, 1).contiguous() + pos2_t = pos2.permute(0, 2, 1).contiguous() + B,C,N = pos1.shape + if self.knn: + _, idx = pointutils.knn(self.nsample, pos1_t, pos2_t) + else: + idx = query_ball_point(self.radius, self.nsample, pos2_t, pos1_t, get_cnt=False) + + pos2_grouped = pointutils.grouping_operation(pos2, idx) + pos_diff = pos2_grouped - pos1.view(B, -1, N, 1) # [B,3,N1,S] + + feat2_grouped = pointutils.grouping_operation(feature2, idx) + feat_new = torch.cat([feat2_grouped, pos_diff], dim = 1) # [B,C1+3,N1,S] + for conv in self.mlp1_convs: + feat_new = conv(feat_new) + # max pooling + feat_new = feat_new.max(-1)[0] # [B,mlp1[-1],N1] + # concatenate feature in early layer + if feature1 is not None: + feat_new = torch.cat([feat_new, feature1], dim=1) + # feat_new = feat_new.view(B,-1,N,1) + for conv in self.mlp2_convs: + feat_new = conv(feat_new) + + return feat_new + +class PointNetFeaturePropogation(nn.Module): + def __init__(self, in_channel, mlp): + super(PointNetFeaturePropogation, self).__init__() + self.mlp_convs = nn.ModuleList() + self.mlp_bns = nn.ModuleList() + last_channel = in_channel + for out_channel in mlp: + self.mlp_convs.append(nn.Conv1d(last_channel, out_channel, 1)) + self.mlp_bns.append(nn.BatchNorm1d(out_channel)) + last_channel = out_channel + + def forward(self, pos1, pos2, feature1, feature2): + """ + Input: + xyz1: input points position data, [B, C, N] + xyz2: sampled input points position data, [B, C, S] + points1: input points data, [B, D, N] + points2: input points data, [B, D, S] + Return: + new_points: upsampled points data, [B, D', N] + """ + pos1_t = pos1.permute(0, 2, 1).contiguous() + pos2_t = pos2.permute(0, 2, 1).contiguous() + B, C, N = pos1.shape + + # dists = square_distance(pos1, pos2) + # dists, idx = dists.sort(dim=-1) + # dists, idx = dists[:, :, :3], idx[:, :, :3] # [B, N, 3] + dists,idx = pointutils.three_nn(pos1_t,pos2_t) + dists[dists < 1e-10] = 1e-10 + weight = 1.0 / dists + weight = weight / torch.sum(weight, -1,keepdim = True) # [B,N,3] + interpolated_feat = torch.sum(pointutils.grouping_operation(feature2, idx) * weight.view(B, 1, N, 3), dim = -1) # [B,C,N,3] + + if feature1 is not None: + feat_new = torch.cat([interpolated_feat, feature1], 1) + else: + feat_new = interpolated_feat + + for i, conv in enumerate(self.mlp_convs): + bn = self.mlp_bns[i] + feat_new = F.relu(bn(conv(feat_new))) + return feat_new + + +class FlowNet3D(nn.Module): + def __init__(self): + super(FlowNet3D, self).__init__() + + self.sa1 = PointNetSetAbstraction(npoint=1024, radius=0.5, nsample=16, in_channel=3, mlp=[32,32,64], group_all=False) + self.sa2 = PointNetSetAbstraction(npoint=256, radius=1.0, nsample=16, in_channel=64, mlp=[64, 64, 128], group_all=False) + self.sa3 = PointNetSetAbstraction(npoint=64, radius=2.0, nsample=8, in_channel=128, mlp=[128, 128, 256], group_all=False) + self.sa4 = PointNetSetAbstraction(npoint=16, radius=4.0, nsample=8, in_channel=256, mlp=[256, 256, 512], group_all=False) + + self.fe_layer = FlowEmbedding(radius=10.0, nsample=64, in_channel = 128, mlp=[128, 128, 128], pooling='max', corr_func='concat') + + self.su1 = PointNetSetUpConv(nsample=8, radius=2.4, f1_channel = 256, f2_channel = 512, mlp=[], mlp2=[256, 256]) + self.su2 = PointNetSetUpConv(nsample=8, radius=1.2, f1_channel = 128+128, f2_channel = 256, mlp=[128, 128, 256], mlp2=[256]) + self.su3 = PointNetSetUpConv(nsample=8, radius=0.6, f1_channel = 64, f2_channel = 256, mlp=[128, 128, 256], mlp2=[256]) + self.fp = PointNetFeaturePropogation(in_channel = 256+3, mlp = [256, 256]) + + self.conv1 = nn.Conv1d(256, 128, kernel_size=1, bias=False) + self.bn1 = nn.BatchNorm1d(128) + self.conv2=nn.Conv1d(128, 3, kernel_size=1, bias=True) + + def forward(self, pc1, pc2, feature1, feature2): + l1_pc1, l1_feature1 = self.sa1(pc1, feature1) + l2_pc1, l2_feature1 = self.sa2(l1_pc1, l1_feature1) + + l1_pc2, l1_feature2 = self.sa1(pc2, feature2) + l2_pc2, l2_feature2 = self.sa2(l1_pc2, l1_feature2) + + _, l2_feature1_new = self.fe_layer(l2_pc1, l2_pc2, l2_feature1, l2_feature2) + + l3_pc1, l3_feature1 = self.sa3(l2_pc1, l2_feature1_new) + l4_pc1, l4_feature1 = self.sa4(l3_pc1, l3_feature1) + + l3_fnew1 = self.su1(l3_pc1, l4_pc1, l3_feature1, l4_feature1) + l2_fnew1 = self.su2(l2_pc1, l3_pc1, torch.cat([l2_feature1, l2_feature1_new], dim=1), l3_fnew1) + l1_fnew1 = self.su3(l1_pc1, l2_pc1, l1_feature1, l2_fnew1) + l0_fnew1 = self.fp(pc1, l1_pc1, feature1, l1_fnew1) + + x = F.relu(self.bn1(self.conv1(l0_fnew1))) + sf = self.conv2(x) + return sf + +if __name__ == '__main__': + import os + import torch + os.environ["CUDA_VISIBLE_DEVICES"] = '0' + input = torch.randn((8,3,2048)) + label = torch.randn(8,16) + model = FlowNet3D() + output = model(input,input) + print(output.size()) \ No newline at end of file diff --git a/thirdparty/learning3d/models/masknet.py b/thirdparty/learning3d/models/masknet.py new file mode 100644 index 0000000000000000000000000000000000000000..033924cb2184dd31b99b4de111273ea27191c5c0 --- /dev/null +++ b/thirdparty/learning3d/models/masknet.py @@ -0,0 +1,84 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F +from .pointnet import PointNet +from .pooling import Pooling + +class PointNetMask(nn.Module): + def __init__(self, template_feature_size=1024, source_feature_size=1024, feature_model=PointNet()): + super().__init__() + self.feature_model = feature_model + self.pooling = Pooling() + + input_size = template_feature_size + source_feature_size + self.h3 = nn.Sequential(nn.Conv1d(input_size, 1024, 1), nn.ReLU(), + nn.Conv1d(1024, 512, 1), nn.ReLU(), + nn.Conv1d(512, 256, 1), nn.ReLU(), + nn.Conv1d(256, 128, 1), nn.ReLU(), + nn.Conv1d(128, 1, 1), nn.Sigmoid()) + + def find_mask(self, x, t_out_h1): + batch_size, _ , num_points = t_out_h1.size() + x = x.unsqueeze(2) + x = x.repeat(1,1,num_points) + x = torch.cat([t_out_h1, x], dim=1) + x = self.h3(x) + return x.view(batch_size, -1) + + def forward(self, template, source): + source_features = self.feature_model(source) # [B x C x N] + template_features = self.feature_model(template) # [B x C x N] + + source_features = self.pooling(source_features) + mask = self.find_mask(source_features, template_features) + return mask + + +class MaskNet(nn.Module): + def __init__(self, feature_model=PointNet(use_bn=True), is_training=True): + super().__init__() + self.maskNet = PointNetMask(feature_model=feature_model) + self.is_training = is_training + + @staticmethod + def index_points(points, idx): + """ + Input: + points: input points data, [B, N, C] + idx: sample index data, [B, S] + Return: + new_points:, indexed points data, [B, S, C] + """ + device = points.device + B = points.shape[0] + view_shape = list(idx.shape) + view_shape[1:] = [1] * (len(view_shape) - 1) + repeat_shape = list(idx.shape) + repeat_shape[0] = 1 + batch_indices = torch.arange(B, dtype=torch.long).to(device).view(view_shape).repeat(repeat_shape) + new_points = points[batch_indices, idx, :] + return new_points + + # This function is only useful for testing with a single pair of point clouds. + @staticmethod + def find_index(mask_val): + mask_idx = torch.nonzero((mask_val[0]>0.5)*1.0) + return mask_idx.view(1, -1) + + def forward(self, template, source, point_selection='threshold'): + mask = self.maskNet(template, source) + + if point_selection == 'topk' or self.is_training: + _, self.mask_idx = torch.topk(mask, source.shape[1], dim=1, sorted=False) + elif point_selection == 'threshold': + self.mask_idx = self.find_index(mask) + + template = self.index_points(template, self.mask_idx) + return template, mask + + +if __name__ == '__main__': + template, source = torch.rand(10,1024,3), torch.rand(10,1024,3) + net = MaskNet() + result = net(template, source) + import ipdb; ipdb.set_trace() \ No newline at end of file diff --git a/thirdparty/learning3d/models/masknet2.py b/thirdparty/learning3d/models/masknet2.py new file mode 100644 index 0000000000000000000000000000000000000000..7a4666403c031bbfe7881710c844f9f997b2cfc1 --- /dev/null +++ b/thirdparty/learning3d/models/masknet2.py @@ -0,0 +1,264 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F +from .pooling import Pooling + + +# Mish Activation Function +class Mish(nn.Module): + def __init__(self): + super(Mish, self).__init__() + + def forward(self, x): + return x * torch.tanh(F.softplus(x)) + + +# Basic Convolution Block +class BasicConv1D(nn.Module): + def __init__(self, in_channels, out_channels, kernel_size=1, stride=1, active = True): + super(BasicConv1D, self).__init__() + self.active = active + self.bn = nn.BatchNorm1d( out_channels) + if self.active == True: + self.activation = Mish() + self.conv = nn.Conv1d(in_channels, out_channels, kernel_size, stride, bias=False) + #self.dropout = nn.Dropout(0.5) + + def forward(self, x): + x = self.conv(x) + x = self.bn(x) + if self.active == True: + x = self.activation(x) + return x + + +class Self_Attn(nn.Module): + """ Self attention Layer""" + def __init__(self, in_dim, out_dim): + super(Self_Attn,self).__init__() + + self.in_dim = in_dim + self.out_dim = out_dim + + # Query Convolution + self.query_conv =BasicConv1D(in_dim, out_dim) + + self.beta = nn.Parameter(torch.zeros(1)) + + self.softmax = nn.Softmax(dim=-1) # + + def forward(self,x): + """ + inputs : + x : input feature maps( B X C X N) 32, 1024, 64 + returns : + out : self attention value + input feature + attention: B X N X N (N is Width*Height) + """ + + proj_query = self.query_conv(x).permute(0,2,1) # B, in_dim, N ---> B, in_dim // 8, N ----> B, N, in_dim // 8 + proj_key = proj_query.permute(0,2,1) #B, in_dim, N ---> B, in_dim // 8, N + + energy = torch.bmm(proj_query,proj_key) # transpose check B, N, N + + attention = self.softmax(energy) # B , N, N + + out_x = torch.bmm(proj_key, attention.permute(0,2,1) ) #B, out_dim, N + + out = self.beta * out_x + proj_key + + return out + +class PointNet(torch.nn.Module): + def __init__(self, emb_dims=224, input_shape="bnc", use_bn=False, global_feat=True): + # emb_dims: Embedding Dimensions for PointNet. + # input_shape: Shape of Input Point Cloud (b: batch, n: no of points, c: channels) + super(PointNet, self).__init__() + if input_shape not in ["bcn", "bnc"]: + raise ValueError("Allowed shapes are 'bcn' (batch * channels * num_in_points), 'bnc' ") + self.input_shape = input_shape + self.emb_dims = emb_dims + self.use_bn = use_bn + self.global_feat = global_feat + if not self.global_feat: self.pooling = Pooling('max') + + self.conv1 = Self_Attn(3, 32) + self.conv2 = Self_Attn(32, 64) + self.conv3 = Self_Attn(64, 64) + self.conv4 = Self_Attn(64, 128) + self.conv5 = Self_Attn(128, self.emb_dims) + + + def forward(self, input_data): + # input_data: Point Cloud having shape input_shape. + # output: PointNet features (Batch x emb_dims) + + if self.input_shape == "bnc": + num_points = input_data.shape[1] + input_data = input_data.permute(0, 2, 1) + else: + num_points = input_data.shape[2] + if input_data.shape[1] != 3: + raise RuntimeError("shape of x must be of [Batch x 3 x NumInPoints]") + + output = input_data + + x1 = self.conv1(output) #32 + x2 = self.conv2(x1) #64 + x3 = self.conv3(x2) #64 + x4 = self.conv4(x3+x2) #128 + x5 = self.conv5(x4) + + output = torch.cat([ x1, x2, x3, x4, x5], dim=1) #256, x4 x0, + point_feature = output + + if self.global_feat: + return output + else: + output = self.pooling(output) + output = output.view(-1, self.emb_dims, 1).repeat(1, 1, num_points) + return torch.cat([output, point_feature], 1) + + +# self attention mechanism +class self_attention_fc(nn.Module): + """ Self attention Layer""" + def __init__(self,in_dim, out_dim): #1024 + super(self_attention_fc,self).__init__() + + self.in_dim = in_dim + self.out_dim = out_dim + + self.query_conv = BasicConv1D(in_dim, out_dim) + + self.beta = nn.Parameter(torch.zeros(1)) + self.softmax = nn.Softmax(dim=-1) # + + def forward(self,x, y): #B, 1024 , 1 + """ + inputs : + x : input feature maps( B X C,1 ) + returns : + out : self attention value + input feature + attention: B X N X N (N is Width*Height) + """ + proj_query_x = self.query_conv(x) #[B, in_dim, 1]----->[B, out_dim1, 1] + + proj_key_y = self.query_conv(y).permute(0,2,1) #[B, 1, out_dim1] + + energy_xy = torch.bmm(proj_query_x, proj_key_y) # xi Attention scores for all points in y [B, 64, 64] + + attention_xy = self.softmax(energy_xy) + attention_yx = self.softmax(energy_xy.permute(0,2,1)) + + proj_value_x = proj_query_x # self.value_conv_x(x) # [B, out_dim, 64] + proj_value_y = proj_key_y.permute(0,2,1) # self.value_conv_x(y) # [B, out_dim, 64] + + out_x = torch.bmm(attention_xy, proj_value_x) # [B, out_dim] + out_x = self.beta* out_x + proj_value_x # self.kama* + + out_y = torch.bmm(attention_yx, proj_value_y ) # [B, out_dim] + out_y = self.beta*out_y + proj_value_y # self.kama * + + return out_x, out_y + + + +class PointNetMask(nn.Module): + def __init__(self, template_feature_size=1024, source_feature_size=1024, feature_model=PointNet()): + super().__init__() + self.feature_model = feature_model + self.pooling_max = Pooling(pool_type='max') + self.pooling_avg = Pooling(pool_type='avg') + + input_size = template_feature_size + source_feature_size + + self.global_feat_1 = self_attention_fc(1024, 512) + self.global_feat_2 = self_attention_fc(512, 256) + self.global_feat_3 = self_attention_fc(256, 512) + + self.h3 = nn.Sequential(BasicConv1D(1024, 512), + BasicConv1D(512, 256), + BasicConv1D(256, 128), + nn.Conv1d(128, 1, 1), nn.Sigmoid()) + + + def find_mask(self, source_features, template_features): + global_source_features_max = self.pooling_max(source_features) + global_template_features_max = self.pooling_max(template_features) + global_source_features_avg = self.pooling_avg(source_features) + global_template_features_avg = self.pooling_avg(template_features) + global_source_features = torch.cat([global_source_features_max, global_source_features_avg], dim=1) + global_template_features = torch.cat([global_template_features_max, global_template_features_avg], dim=1) + + shared_feat_1,shared_feat_2 = self.global_feat_1(global_source_features.unsqueeze(2), global_template_features.unsqueeze(2)) + shared_feat_1,shared_feat_2 = self.global_feat_2(shared_feat_1, shared_feat_2) + shared_feat_1,shared_feat_2 = self.global_feat_3(shared_feat_1, shared_feat_2) + + batch_size, _ , num_points = source_features.size() + global_source_features = shared_feat_1 + global_source_features = global_source_features.repeat(1,1,num_points) + x = torch.cat([template_features, global_source_features], dim=1) + x = self.h3(x) + + batch_size, _ , num_points = template_features.size() + global_template_features = shared_feat_2 + global_template_features = global_template_features.repeat(1,1,num_points) + y = torch.cat([source_features, global_template_features], dim=1) + y = self.h3(y) + + return x.view(batch_size, -1), y.view(batch_size, -1) + + def forward(self, template, source): + source_features = self.feature_model(source) # [B x C x N] + template_features = self.feature_model(template) # [B x C x N] + + template_mask, source_mask = self.find_mask(source_features, template_features) + return template_mask, source_mask + +class MaskNet2(nn.Module): + def __init__(self, feature_model=PointNet(use_bn=True), is_training=True): + super().__init__() + self.maskNet = PointNetMask(feature_model=feature_model) + self.is_training = is_training + + @staticmethod + def index_points(points, idx): + """ + Input: + points: input points data, [B, N, C] + idx: sample index data, [B, S] + Return: + new_points:, indexed points data, [B, S, C] + """ + device = points.device + B = points.shape[0] + view_shape = list(idx.shape) + view_shape[1:] = [1] * (len(view_shape) - 1) + repeat_shape = list(idx.shape) + repeat_shape[0] = 1 + batch_indices = torch.arange(B, dtype=torch.long).to(device).view(view_shape).repeat(repeat_shape) + new_points = points[batch_indices, idx, :] + + return new_points + + def forward(self, template, source, point_selection='threshold', mask_threshold = 0.5): + template_mask, source_mask = self.maskNet(template, source) #B, N + if not torch.cuda.is_available(): + device = 'cpu' + device = torch.device(device) + + source_binary_mask = torch.where(source_mask > mask_threshold, torch.ones(source_mask.size()).to(device), torch.zeros(source_mask.size()).to(device)) + template_binary_mask = torch.where(template_mask > mask_threshold, torch.ones(template_mask.size()).to(device), torch.zeros(template_mask.size()).to(device)) + + masked_template = template[:, torch.tensor(template_binary_mask, dtype = torch.bool).squeeze(0), 0:3] + masked_source = source[:, torch.tensor(source_binary_mask, dtype = torch.bool).squeeze(0), 0:3] + + return masked_template, masked_source, template_mask, source_mask + + +if __name__ == '__main__': + template, source = torch.rand(10,1024,3), torch.rand(10,1024,3) + net = MaskNet2() + result = net(template, source) + import ipdb; ipdb.set_trace() \ No newline at end of file diff --git a/thirdparty/learning3d/models/pcn.py b/thirdparty/learning3d/models/pcn.py new file mode 100644 index 0000000000000000000000000000000000000000..d223ce5543b65828ac83b25ac87c4c9770a17da3 --- /dev/null +++ b/thirdparty/learning3d/models/pcn.py @@ -0,0 +1,164 @@ +# author: Vinit Sarode (vinitsarode5@gmail.com) 03/23/2020 + +import torch +import torch.nn as nn +import torch.nn.functional as F +from .pooling import Pooling + +class PCN(torch.nn.Module): + def __init__(self, emb_dims=1024, input_shape="bnc", num_coarse=1024, grid_size=4, detailed_output=False): + # emb_dims: Embedding Dimensions for PCN. + # input_shape: Shape of Input Point Cloud (b: batch, n: no of points, c: channels) + super(PCN, self).__init__() + if input_shape not in ["bcn", "bnc"]: + raise ValueError("Allowed shapes are 'bcn' (batch * channels * num_in_points), 'bnc' ") + self.input_shape = input_shape + self.emb_dims = emb_dims + self.num_coarse = num_coarse + self.detailed_output = detailed_output + self.grid_size = grid_size + self.num_fine = self.grid_size ** 2 * self.num_coarse + self.pooling = Pooling('max') + + self.encoder() + self.decoder_layers = self.decoder() + if detailed_output: self.folding_layers = self.folding() + + def encoder_1(self): + self.conv1 = torch.nn.Conv1d(3, 128, 1) + self.conv2 = torch.nn.Conv1d(128, 256, 1) + self.relu = torch.nn.ReLU() + + # self.bn1 = torch.nn.BatchNorm1d(128) + # self.bn2 = torch.nn.BatchNorm1d(256) + + layers = [self.conv1, self.relu, + self.conv2] + return layers + + def encoder_2(self): + self.conv3 = torch.nn.Conv1d(2*256, 512, 1) + self.conv4 = torch.nn.Conv1d(512, self.emb_dims, 1) + + # self.bn3 = torch.nn.BatchNorm1d(512) + # self.bn4 = torch.nn.BatchNorm1d(self.emb_dims) + self.relu = torch.nn.ReLU() + + layers = [self.conv3, self.relu, + self.conv4] + return layers + + def encoder(self): + self.encoder_layers1 = self.encoder_1() + self.encoder_layers2 = self.encoder_2() + + def decoder(self): + self.linear1 = torch.nn.Linear(self.emb_dims, 1024) + self.linear2 = torch.nn.Linear(1024, 1024) + self.linear3 = torch.nn.Linear(1024, self.num_coarse*3) + + # self.bn1 = torch.nn.BatchNorm1d(1024) + # self.bn2 = torch.nn.BatchNorm1d(1024) + # self.bn3 = torch.nn.BatchNorm1d(self.num_coarse*3) + self.relu = torch.nn.ReLU() + + layers = [self.linear1, self.relu, + self.linear2, self.relu, + self.linear3] + return layers + + def folding(self): + self.conv5 = torch.nn.Conv1d(1029, 512, 1) + self.conv6 = torch.nn.Conv1d(512, 512, 1) + self.conv7 = torch.nn.Conv1d(512, 3, 1) + + # self.bn5 = torch.nn.BatchNorm1d(512) + # self.bn6 = torch.nn.BatchNorm1d(512) + self.relu = torch.nn.ReLU() + + layers = [self.conv5, self.relu, + self.conv6, self.relu, + self.conv7] + return layers + + def fine_decoder(self): + # Fine Output + linspace = torch.linspace(-0.05, 0.05, steps=self.grid_size) + grid = torch.meshgrid(linspace, linspace) + grid = torch.reshape(torch.stack(grid, dim=2), (-1,2)) # 16x2 + grid = torch.unsqueeze(grid, dim=0) # 1x16x2 + grid_feature = grid.repeat([self.coarse_output.shape[0], self.num_coarse, 1]) # Bx16384x2 + + point_feature = torch.unsqueeze(self.coarse_output, dim=2) # Bx1024x1x3 + point_feature = point_feature.repeat([1, 1, self.grid_size ** 2, 1]) # Bx1024x16x3 + point_feature = torch.reshape(point_feature, (-1, self.num_fine, 3)) # Bx16384x3 + + global_feature = torch.unsqueeze(self.global_feature_v, dim=1) # Bx1x1024 + global_feature = global_feature.repeat([1, self.num_fine, 1]) # Bx16384x1024 + + feature = torch.cat([grid_feature, point_feature, global_feature], dim=2) # Bx16384x1029 + + center = torch.unsqueeze(self.coarse_output, dim=2) # Bx1024x1x3 + center = center.repeat([1, 1, self.grid_size ** 2, 1]) # Bx1024x16x3 + center = torch.reshape(center, [-1, self.num_fine, 3]) # Bx16384x3 + + output = feature.permute(0, 2, 1) + for idx, layer in enumerate(self.folding_layers): + output = layer(output) + fine_output = output.permute(0, 2, 1) + center + return fine_output + + def encode(self, input_data): + output = input_data + for idx, layer in enumerate(self.encoder_layers1): + output = layer(output) + + global_feature_g = self.pooling(output) + + global_feature_g = global_feature_g.unsqueeze(2) + global_feature_g = global_feature_g.repeat(1,1,self.num_points) + output = torch.cat([output, global_feature_g], dim=1) + + for idx, layer in enumerate(self.encoder_layers2): + output = layer(output) + + self.global_feature_v = self.pooling(output) + + def decode(self): + output = self.global_feature_v + for idx, layer in enumerate(self.decoder_layers): + output = layer(output) + self.coarse_output = output.view(self.global_feature_v.shape[0], self.num_coarse, 3) + + def forward(self, input_data): + # input_data: Point Cloud having shape input_shape. + # output: PointNet features (Batch x emb_dims) + if self.input_shape == "bnc": + self.num_points = input_data.shape[1] + input_data = input_data.permute(0, 2, 1) + else: + self.num_points = input_data.shape[2] + if input_data.shape[1] != 3: + raise RuntimeError("shape of x must be of [Batch x 3 x NumInPoints]") + + self.encode(input_data) + self.decode() + + result = {'coarse_output': self.coarse_output} + + if self.detailed_output: + fine_output = self.fine_decoder() + result['fine_output'] = fine_output + + return result + + +if __name__ == '__main__': + # Test the code. + x = torch.rand((10,1024,3)) + + pcn = PCN() + y = pcn(x) + print("Network Architecture: ") + print(pn) + print("Input Shape of PCN: ", x.shape, "\nOutput Shape of PCN: ", y['coarse_output'].shape) \ No newline at end of file diff --git a/thirdparty/learning3d/models/pcrnet.py b/thirdparty/learning3d/models/pcrnet.py new file mode 100644 index 0000000000000000000000000000000000000000..59ef023cf729db21365aa47724e410d95ae4411b --- /dev/null +++ b/thirdparty/learning3d/models/pcrnet.py @@ -0,0 +1,74 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F +from .pointnet import PointNet +from .pooling import Pooling +from .. ops.transform_functions import PCRNetTransform as transform + + +class iPCRNet(nn.Module): + def __init__(self, feature_model=PointNet(), droput=0.0, pooling='max'): + super().__init__() + self.feature_model = feature_model + self.pooling = Pooling(pooling) + + self.linear = [nn.Linear(self.feature_model.emb_dims * 2, 1024), nn.ReLU(), + nn.Linear(1024, 1024), nn.ReLU(), + nn.Linear(1024, 512), nn.ReLU(), + nn.Linear(512, 512), nn.ReLU(), + nn.Linear(512, 256), nn.ReLU()] + + if droput>0.0: + self.linear.append(nn.Dropout(droput)) + self.linear.append(nn.Linear(256,7)) + + self.linear = nn.Sequential(*self.linear) + + # Single Pass Alignment Module (SPAM) + def spam(self, template_features, source, est_R, est_t): + batch_size = source.size(0) + + self.source_features = self.pooling(self.feature_model(source)) + y = torch.cat([template_features, self.source_features], dim=1) + pose_7d = self.linear(y) + pose_7d = transform.create_pose_7d(pose_7d) + + # Find current rotation and translation. + identity = torch.eye(3).to(source).view(1,3,3).expand(batch_size, 3, 3).contiguous() + est_R_temp = transform.quaternion_rotate(identity, pose_7d).permute(0, 2, 1) + est_t_temp = transform.get_translation(pose_7d).view(-1, 1, 3) + + # update translation matrix. + est_t = torch.bmm(est_R_temp, est_t.permute(0, 2, 1)).permute(0, 2, 1) + est_t_temp + # update rotation matrix. + est_R = torch.bmm(est_R_temp, est_R) + + source = transform.quaternion_transform(source, pose_7d) # Ps' = est_R*Ps + est_t + return est_R, est_t, source + + def forward(self, template, source, max_iteration=8): + est_R = torch.eye(3).to(template).view(1, 3, 3).expand(template.size(0), 3, 3).contiguous() # (Bx3x3) + est_t = torch.zeros(1,3).to(template).view(1, 1, 3).expand(template.size(0), 1, 3).contiguous() # (Bx1x3) + template_features = self.pooling(self.feature_model(template)) + + if max_iteration == 1: + est_R, est_t, source = self.spam(template_features, source, est_R, est_t) + else: + for i in range(max_iteration): + est_R, est_t, source = self.spam(template_features, source, est_R, est_t) + + result = {'est_R': est_R, # source -> template + 'est_t': est_t, # source -> template + 'est_T': transform.convert2transformation(est_R, est_t), # source -> template + 'r': template_features - self.source_features, + 'transformed_source': source} + return result + + +if __name__ == '__main__': + template, source = torch.rand(10,1024,3), torch.rand(10,1024,3) + pn = PointNet() + + net = iPCRNet(pn) + result = net(template, source) + import ipdb; ipdb.set_trace() \ No newline at end of file diff --git a/thirdparty/learning3d/models/pointconv.py b/thirdparty/learning3d/models/pointconv.py new file mode 100644 index 0000000000000000000000000000000000000000..e550f13a90b6f75078f377b6f534e5a048513819 --- /dev/null +++ b/thirdparty/learning3d/models/pointconv.py @@ -0,0 +1,108 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F +from .. utils import PointConvDensitySetAbstraction + +class PointConvDensityClsSsg(torch.nn.Module): + def __init__(self, emb_dims=1024, input_shape="bnc", input_channel_dim=3, classifier=False, num_classes=40, pretrained=None): + super(PointConvDensityClsSsg, self).__init__() + if input_shape not in ["bnc", "bcn"]: + raise ValueError("Allowed shapes are 'bcn' (batch * channels * num_in_points), 'bnc' ") + self.input_shape = input_shape + self.emb_dims = emb_dims + self.classifier = classifier + self.input_channel_dim = input_channel_dim + self.create_structure() + if self.classifier: self.create_classifier(num_classes) + + def create_structure(self): + # Arguments to define PointConv network using PointConvDensitySetAbstraction class. + # npoint: number of points sampled from input. + # nsample: number of neighbours chosen for each point in sampled point cloud. + # in_channel: number of channels in input. + # mlp: sizes of multi-layer perceptrons. + # bandwidth: used to compute gaussian density. + # group_all: group all points from input to a single point if set to True. + self.sa1 = PointConvDensitySetAbstraction(npoint=512, nsample=32, in_channel=self.input_channel_dim, + mlp=[64, 64, 128], bandwidth = 0.1, group_all=False) + self.sa2 = PointConvDensitySetAbstraction(npoint=128, nsample=64, in_channel=128 + 3, + mlp=[128, 128, 256], bandwidth = 0.2, group_all=False) + self.sa3 = PointConvDensitySetAbstraction(npoint=1, nsample=None, in_channel=256 + 3, + mlp=[256, 512, self.emb_dims], bandwidth = 0.4, group_all=True) + + def create_classifier(self, num_classes): + # These are simple fully-connected layers with batch-norm and dropouts. + # This architecture is given by PointConv paper. Hence, I used it here as a default version. + # This can be easily modified by overwriting this function or by using classifier.py class. + self.fc1 = nn.Linear(self.emb_dims, 512) + self.bn1 = nn.BatchNorm1d(512) + self.drop1 = nn.Dropout(0.7) + self.fc2 = nn.Linear(512, 256) + self.bn2 = nn.BatchNorm1d(256) + self.drop2 = nn.Dropout(0.7) + self.fc3 = nn.Linear(256, num_classes) + + def forward(self, input_data): + if self.input_shape == "bnc": + input_data = input_data.permute(0, 2, 1) + batch_size = input_data.shape[0] + + # Convert point clouds to latent features using PointConv network. + l1_points, l1_features = self.sa1(input_data[:, :3, :], input_data[:, 3:, :]) + l2_points, l2_features = self.sa2(l1_points, l1_features) + l3_points, l3_features = self.sa3(l2_points, l2_features) + features = l3_features.view(batch_size, self.emb_dims) + + if self.classifier: + # Use these features to classify the input point cloud. + features = self.drop1(F.relu(self.bn1(self.fc1(features)))) + features = self.drop2(F.relu(self.bn2(self.fc2(features)))) + features = self.fc3(features) + output = F.log_softmax(features, -1) + else: + # Return the PointConv features for the use of other higher level tasks. + output = features + + return output + +def create_pointconv(classifier=False, pretrained=None): + if classifier and pretrained is not None: + class Network(torch.nn.Module): + def __init__(self, emb_dims=1024, input_shape="bnc", input_channel_dim=3, classifier=False, num_classes=40, pretrained=None): + # Arguments: + # emb_dims: Size of embeddings. + # input_shape: Shape of input point cloud. + # input_channel_dim: Number of channels in point cloud. [eg. Nx3 (only points) or Nx6 (points + normals)] + # classifier: Do you want to use default classifier layers or just the embedding layers. + # num_classes: If you use classifier then decide the number of classes in your dataset. + # use_pretrained: Use pretrained classification network. + super(PointConv, self).__init__() + self.pointconv = PointConvDensityClsSsg(emb_dims, input_shape, input_channel_dim, classifier, num_classes) + # super().__init__(emb_dims, input_shape, input_channel_dim, classifier, num_classes) + if classifier and pretrained is not None: + self.use_pretrained(pretrained) + + def use_pretrained(self, pretrained): + checkpoint = torch.load(pretrained, map_location='cpu') + self.pointconv.load_state_dict(checkpoint['model_state_dict']) + + def forward(self, input_data): + return self.pointconv(input_data) + return Network + else: + class Network(PointConvDensityClsSsg): + def __init__(self, emb_dims=1024, input_shape="bnc", input_channel_dim=3, classifier=False, num_classes=40, pretrained=None): + super().__init__(emb_dims=emb_dims, input_shape=input_shape, input_channel_dim=input_channel_dim, classifier=classifier, num_classes=num_classes, pretrained=pretrained) + return Network + + +if __name__ == '__main__': + # Test the code. + x = torch.rand((2,1024,3)) + + PointConv = create_pointconv(classifier=False, pretrained='checkpoint.pth') + pc = PointConv(input_channel_dim=3, classifier=False, pretrained='checkpoint.pth') + y = pc(x) + print("Network Architecture: ") + print(pc) + print("Input Shape of PointNet: ", x.shape, "\nOutput Shape of PointNet: ", y.shape) \ No newline at end of file diff --git a/thirdparty/learning3d/models/pointnet.py b/thirdparty/learning3d/models/pointnet.py new file mode 100644 index 0000000000000000000000000000000000000000..32308ab46fd754b919be4a81927f488593ac0fbe --- /dev/null +++ b/thirdparty/learning3d/models/pointnet.py @@ -0,0 +1,108 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F +from .pooling import Pooling + + +class PointNet(torch.nn.Module): + def __init__(self, emb_dims=1024, input_shape="bnc", use_bn=False, global_feat=True): + # emb_dims: Embedding Dimensions for PointNet. + # input_shape: Shape of Input Point Cloud (b: batch, n: no of points, c: channels) + super(PointNet, self).__init__() + if input_shape not in ["bcn", "bnc"]: + raise ValueError("Allowed shapes are 'bcn' (batch * channels * num_in_points), 'bnc' ") + self.input_shape = input_shape + self.emb_dims = emb_dims + self.use_bn = use_bn + self.global_feat = global_feat + if not self.global_feat: self.pooling = Pooling('max') + + self.layers = self.create_structure() + + def create_structure(self): + self.conv1 = torch.nn.Conv1d(3, 64, 1) + self.conv2 = torch.nn.Conv1d(64, 64, 1) + self.conv3 = torch.nn.Conv1d(64, 64, 1) + self.conv4 = torch.nn.Conv1d(64, 128, 1) + self.conv5 = torch.nn.Conv1d(128, self.emb_dims, 1) + self.relu = torch.nn.ReLU() + + if self.use_bn: + self.bn1 = torch.nn.BatchNorm1d(64) + self.bn2 = torch.nn.BatchNorm1d(64) + self.bn3 = torch.nn.BatchNorm1d(64) + self.bn4 = torch.nn.BatchNorm1d(128) + self.bn5 = torch.nn.BatchNorm1d(self.emb_dims) + + if self.use_bn: + layers = [self.conv1, self.bn1, self.relu, + self.conv2, self.bn2, self.relu, + self.conv3, self.bn3, self.relu, + self.conv4, self.bn4, self.relu, + self.conv5, self.bn5, self.relu] + else: + layers = [self.conv1, self.relu, + self.conv2, self.relu, + self.conv3, self.relu, + self.conv4, self.relu, + self.conv5, self.relu] + return layers + + + def forward(self, input_data): + # input_data: Point Cloud having shape input_shape. + # output: PointNet features (Batch x emb_dims) + if self.input_shape == "bnc": + num_points = input_data.shape[1] + input_data = input_data.permute(0, 2, 1) + else: + num_points = input_data.shape[2] + if input_data.shape[1] != 3: + raise RuntimeError("shape of x must be of [Batch x 3 x NumInPoints]") + + output = input_data + for idx, layer in enumerate(self.layers): + output = layer(output) + if idx == 1 and not self.global_feat: point_feature = output + + if self.global_feat: + return output + else: + output = self.pooling(output) + output = output.view(-1, self.emb_dims, 1).repeat(1, 1, num_points) + return torch.cat([output, point_feature], 1) + + +if __name__ == '__main__': + # Test the code. + x = torch.rand((10,1024,3)) + + pn = PointNet(use_bn=True) + y = pn(x) + print("Network Architecture: ") + print(pn) + print("Input Shape of PointNet: ", x.shape, "\nOutput Shape of PointNet: ", y.shape) + + class PointNet_modified(PointNet): + def __init__(self): + super().__init__() + + def create_structure(self): + self.conv1 = torch.nn.Conv1d(3, 64, 1) + self.conv2 = torch.nn.Conv1d(64, 128, 1) + self.conv3 = torch.nn.Conv1d(128, self.emb_dims, 1) + self.relu = torch.nn.ReLU() + + layers = [self.conv1, self.relu, + self.conv2, self.relu, + self.conv3, self.relu] + return layers + + pn = PointNet_modified() + y = pn(x) + print("\n\n\nModified Network Architecture: ") + print(pn) + print("Input Shape of PointNet: ", x.shape, "\nOutput Shape of PointNet: ", y.shape) + + + diff --git a/thirdparty/learning3d/models/pointnetlk.py b/thirdparty/learning3d/models/pointnetlk.py new file mode 100644 index 0000000000000000000000000000000000000000..271de5b63018e2000483fd28af350ef0179405fb --- /dev/null +++ b/thirdparty/learning3d/models/pointnetlk.py @@ -0,0 +1,173 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F +from .pointnet import PointNet +from .pooling import Pooling +from .. ops import data_utils +from .. ops import se3, so3, invmat + + +class PointNetLK(nn.Module): + def __init__(self, feature_model=PointNet(), delta=1.0e-2, learn_delta=False, xtol=1.0e-7, p0_zero_mean=True, p1_zero_mean=True, pooling='max'): + super().__init__() + self.feature_model = feature_model + self.pooling = Pooling(pooling) + self.inverse = invmat.InvMatrix.apply + self.exp = se3.Exp # [B, 6] -> [B, 4, 4] + self.transform = se3.transform # [B, 1, 4, 4] x [B, N, 3] -> [B, N, 3] + + w1, w2, w3, v1, v2, v3 = delta, delta, delta, delta, delta, delta + twist = torch.Tensor([w1, w2, w3, v1, v2, v3]) + self.dt = torch.nn.Parameter(twist.view(1, 6), requires_grad=learn_delta) + + # results + self.last_err = None + self.g_series = None # for debug purpose + self.prev_r = None + self.g = None # estimation result + self.itr = 0 + self.xtol = xtol + self.p0_zero_mean = p0_zero_mean + self.p1_zero_mean = p1_zero_mean + + def forward(self, template, source, maxiter=10): + template, source, template_mean, source_mean = data_utils.mean_shift(template, source, + self.p0_zero_mean, self.p1_zero_mean) + + result = self.iclk(template, source, maxiter) + result = data_utils.postprocess_data(result, template, source, template_mean, source_mean, + self.p0_zero_mean, self.p1_zero_mean) + return result + + def iclk(self, template, source, maxiter): + batch_size = template.size(0) + + est_T0 = torch.eye(4).to(template).view(1, 4, 4).expand(template.size(0), 4, 4).contiguous() + est_T = est_T0 + self.est_T_series = torch.zeros(maxiter+1, *est_T0.size(), dtype=est_T0.dtype) + self.est_T_series[0] = est_T0.clone() + + training = self.handle_batchNorm(template, source) + + # re-calc. with current modules + template_features = self.pooling(self.feature_model(template)) # [B, N, 3] -> [B, K] + + # approx. J by finite difference + dt = self.dt.to(template).expand(batch_size, 6) + J = self.approx_Jic(template, template_features, dt) + + self.last_err = None + pinv = self.compute_inverse_jacobian(J, template_features, source) + if pinv == {}: + result = {'est_R': est_T[:,0:3,0:3], + 'est_t': est_T[:,0:3,3], + 'est_T': est_T, + 'r': None, + 'transformed_source': self.transform(est_T.unsqueeze(1), source), + 'itr': 1, + 'est_T_series': self.est_T_series} + return result + + itr = 0 + r = None + for itr in range(maxiter): + self.prev_r = r + transformed_source = self.transform(est_T.unsqueeze(1), source) # [B, 1, 4, 4] x [B, N, 3] -> [B, N, 3] + source_features = self.pooling(self.feature_model(transformed_source)) # [B, N, 3] -> [B, K] + r = source_features - template_features + + pose = -pinv.bmm(r.unsqueeze(-1)).view(batch_size, 6) + + check = pose.norm(p=2, dim=1, keepdim=True).max() + if float(check) < self.xtol: + if itr == 0: + self.last_err = 0 # no update. + break + + est_T = self.update(est_T, pose) + self.est_T_series[itr+1] = est_T.clone() + + rep = len(range(itr, maxiter)) + self.est_T_series[(itr+1):] = est_T.clone().unsqueeze(0).repeat(rep, 1, 1, 1) + + self.feature_model.train(training) + self.est_T = est_T + + result = {'est_R': est_T[:,0:3,0:3], + 'est_t': est_T[:,0:3,3], + 'est_T': est_T, + 'r': r, + 'transformed_source': self.transform(est_T.unsqueeze(1), source), + 'itr': itr+1, + 'est_T_series': self.est_T_series} + + return result + + def update(self, g, dx): + # [B, 4, 4] x [B, 6] -> [B, 4, 4] + dg = self.exp(dx) + return dg.matmul(g) + + def approx_Jic(self, template, template_features, dt): + # p0: [B, N, 3], Variable + # f0: [B, K], corresponding feature vector + # dt: [B, 6], Variable + # Jk = (feature_model(p(-delta[k], p0)) - f0) / delta[k] + + batch_size = template.size(0) + num_points = template.size(1) + + # compute transforms + transf = torch.zeros(batch_size, 6, 4, 4).to(template) + for b in range(template.size(0)): + d = torch.diag(dt[b, :]) # [6, 6] + D = self.exp(-d) # [6, 4, 4] + transf[b, :, :, :] = D[:, :, :] + transf = transf.unsqueeze(2).contiguous() # [B, 6, 1, 4, 4] + p = self.transform(transf, template.unsqueeze(1)) # x [B, 1, N, 3] -> [B, 6, N, 3] + + #f0 = self.feature_model(p0).unsqueeze(-1) # [B, K, 1] + template_features = template_features.unsqueeze(-1) # [B, K, 1] + f = self.pooling(self.feature_model(p.view(-1, num_points, 3))).view(batch_size, 6, -1).transpose(1, 2) # [B, K, 6] + + df = template_features - f # [B, K, 6] + J = df / dt.unsqueeze(1) + + return J + + def compute_inverse_jacobian(self, J, template_features, source): + # compute pinv(J) to solve J*x = -r + try: + Jt = J.transpose(1, 2) # [B, 6, K] + H = Jt.bmm(J) # [B, 6, 6] + B = self.inverse(H) + pinv = B.bmm(Jt) # [B, 6, K] + return pinv + except RuntimeError as err: + # singular...? + self.last_err = err + g = torch.eye(4).to(source).view(1, 4, 4).expand(source.size(0), 4, 4).contiguous() + #print(err) + # Perhaps we can use MP-inverse, but,... + # probably, self.dt is way too small... + source_features = self.pooling(self.feature_model(source)) # [B, N, 3] -> [B, K] + r = source_features - template_features + self.feature_model.train(self.feature_model.training) + return {} + + def handle_batchNorm(self, template, source): + training = self.feature_model.training + if training: + # first, update BatchNorm modules + template_features, source_features = self.pooling(self.feature_model(template)), self.pooling(self.feature_model(source)) + self.feature_model.eval() # and fix them. + return training + + +if __name__ == '__main__': + template, source = torch.rand(10,1024,3), torch.rand(10,1024,3) + pn = PointNet() + + net = PointNetLK(pn) + result = net(template, source) + import ipdb; ipdb.set_trace() \ No newline at end of file diff --git a/thirdparty/learning3d/models/pooling.py b/thirdparty/learning3d/models/pooling.py new file mode 100644 index 0000000000000000000000000000000000000000..fc647ab060a673da7ac1fd15691c89b93d455a57 --- /dev/null +++ b/thirdparty/learning3d/models/pooling.py @@ -0,0 +1,15 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F + + +class Pooling(torch.nn.Module): + def __init__(self, pool_type='max'): + self.pool_type = pool_type + super(Pooling, self).__init__() + + def forward(self, input): + if self.pool_type == 'max': + return torch.max(input, 2)[0].contiguous() + elif self.pool_type == 'avg' or self.pool_type == 'average': + return torch.mean(input, 2).contiguous() \ No newline at end of file diff --git a/thirdparty/learning3d/models/ppfnet.py b/thirdparty/learning3d/models/ppfnet.py new file mode 100644 index 0000000000000000000000000000000000000000..253099b3bebf893ec0bdb1ed20dd11a1822b13ab --- /dev/null +++ b/thirdparty/learning3d/models/ppfnet.py @@ -0,0 +1,102 @@ +"""Feature Extraction and Parameter Prediction networks +""" +import logging +import numpy as np +import torch +import torch.nn as nn +import torch.nn.functional as F + +from .. utils import sample_and_group_multi + +_raw_features_sizes = {'xyz': 3, 'dxyz': 3, 'ppf': 4} +_raw_features_order = {'xyz': 0, 'dxyz': 1, 'ppf': 2} + + +def get_prepool(in_dim, out_dim): + """Shared FC part in PointNet before max pooling""" + net = nn.Sequential( + nn.Conv2d(in_dim, out_dim // 2, 1), + nn.GroupNorm(8, out_dim // 2), + nn.ReLU(), + nn.Conv2d(out_dim // 2, out_dim // 2, 1), + nn.GroupNorm(8, out_dim // 2), + nn.ReLU(), + nn.Conv2d(out_dim // 2, out_dim, 1), + nn.GroupNorm(8, out_dim), + nn.ReLU(), + ) + return net + + +def get_postpool(in_dim, out_dim): + """Linear layers in PointNet after max pooling + + Args: + in_dim: Number of input channels + out_dim: Number of output channels. Typically smaller than in_dim + + """ + net = nn.Sequential( + nn.Conv1d(in_dim, in_dim, 1), + nn.GroupNorm(8, in_dim), + nn.ReLU(), + nn.Conv1d(in_dim, out_dim, 1), + nn.GroupNorm(8, out_dim), + nn.ReLU(), + nn.Conv1d(out_dim, out_dim, 1), + ) + + return net + + +class PPFNet(nn.Module): + """Feature extraction Module that extracts hybrid features""" + def __init__(self, features=['ppf', 'dxyz', 'xyz'], emb_dims=96, radius=0.3, num_neighbors=64): + super().__init__() + + self._logger = logging.getLogger(self.__class__.__name__) + self._logger.info('Using early fusion, feature dim = {}'.format(emb_dims)) + self.radius = radius + self.n_sample = num_neighbors + + self.features = sorted(features, key=lambda f: _raw_features_order[f]) + self._logger.info('Feature extraction using features {}'.format(', '.join(self.features))) + + # Layers + raw_dim = np.sum([_raw_features_sizes[f] for f in self.features]) # number of channels after concat + self.prepool = get_prepool(raw_dim, emb_dims * 2) + self.postpool = get_postpool(emb_dims * 2, emb_dims) + + def forward(self, xyz, normals): + """Forward pass of the feature extraction network + + Args: + xyz: (B, N, 3) + normals: (B, N, 3) + + Returns: + cluster features (B, N, C) + + """ + features = sample_and_group_multi(-1, self.radius, self.n_sample, xyz, normals) + features['xyz'] = features['xyz'][:, :, None, :] + + # Gate and concat + concat = [] + for i in range(len(self.features)): + f = self.features[i] + expanded = (features[f]).expand(-1, -1, self.n_sample, -1) + concat.append(expanded) + fused_input_feat = torch.cat(concat, -1) + + # Prepool_FC, pool, postpool-FC + new_feat = fused_input_feat.permute(0, 3, 2, 1) # [B, 10, n_sample, N] + new_feat = self.prepool(new_feat) + + pooled_feat = torch.max(new_feat, 2)[0] # Max pooling (B, C, N) + + post_feat = self.postpool(pooled_feat) # Post pooling dense layers + cluster_feat = post_feat.permute(0, 2, 1) + cluster_feat = cluster_feat / torch.norm(cluster_feat, dim=-1, keepdim=True) + + return cluster_feat # (B, N, C) \ No newline at end of file diff --git a/thirdparty/learning3d/models/prnet.py b/thirdparty/learning3d/models/prnet.py new file mode 100644 index 0000000000000000000000000000000000000000..1772b28489b921c8c8389408ee3a0705582ae749 --- /dev/null +++ b/thirdparty/learning3d/models/prnet.py @@ -0,0 +1,397 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + + +import os +import sys +import glob +import h5py +import copy +import math +import json +import numpy as np +from tqdm import tqdm +import torch +import torch.nn as nn +import torch.nn.functional as F + +from .. ops import transform_functions as transform +from .. utils import Transformer, Identity, knn, get_graph_feature + +from sklearn.metrics import r2_score + +device = torch.device('cuda') if torch.cuda.is_available() else torch.device('cpu') + + +def pairwise_distance(src, tgt): + inner = -2 * torch.matmul(src.transpose(2, 1).contiguous(), tgt) + xx = torch.sum(src**2, dim=1, keepdim=True) + yy = torch.sum(tgt**2, dim=1, keepdim=True) + distances = xx.transpose(2, 1).contiguous() + inner + yy + return torch.sqrt(distances) + +def cycle_consistency(rotation_ab, translation_ab, rotation_ba, translation_ba): + batch_size = rotation_ab.size(0) + identity = torch.eye(3, device=rotation_ab.device).unsqueeze(0).repeat(batch_size, 1, 1) + return F.mse_loss(torch.matmul(rotation_ab, rotation_ba), identity) + F.mse_loss(translation_ab, -translation_ba) + + +class PointNet(nn.Module): + def __init__(self, emb_dims=512): + super(PointNet, self).__init__() + self.conv1 = nn.Conv1d(3, 64, kernel_size=1, bias=False) + self.conv2 = nn.Conv1d(64, 64, kernel_size=1, bias=False) + self.conv3 = nn.Conv1d(64, 64, kernel_size=1, bias=False) + self.conv4 = nn.Conv1d(64, 128, kernel_size=1, bias=False) + self.conv5 = nn.Conv1d(128, emb_dims, kernel_size=1, bias=False) + self.bn1 = nn.BatchNorm1d(64) + self.bn2 = nn.BatchNorm1d(64) + self.bn3 = nn.BatchNorm1d(64) + self.bn4 = nn.BatchNorm1d(128) + self.bn5 = nn.BatchNorm1d(emb_dims) + + def forward(self, x): + x = F.relu(self.bn1(self.conv1(x))) + x = F.relu(self.bn2(self.conv2(x))) + x = F.relu(self.bn3(self.conv3(x))) + x = F.relu(self.bn4(self.conv4(x))) + x = F.relu(self.bn5(self.conv5(x))) + return x + + +class DGCNN(nn.Module): + def __init__(self, emb_dims=512): + super(DGCNN, self).__init__() + self.conv1 = nn.Conv2d(6, 64, kernel_size=1, bias=False) + self.conv2 = nn.Conv2d(64*2, 64, kernel_size=1, bias=False) + self.conv3 = nn.Conv2d(64*2, 128, kernel_size=1, bias=False) + self.conv4 = nn.Conv2d(128*2, 256, kernel_size=1, bias=False) + self.conv5 = nn.Conv2d(512, emb_dims, kernel_size=1, bias=False) + self.bn1 = nn.BatchNorm2d(64) + self.bn2 = nn.BatchNorm2d(64) + self.bn3 = nn.BatchNorm2d(128) + self.bn4 = nn.BatchNorm2d(256) + self.bn5 = nn.BatchNorm2d(emb_dims) + + def forward(self, x): + batch_size, num_dims, num_points = x.size() + x = get_graph_feature(x, device=device) + x = F.leaky_relu(self.bn1(self.conv1(x)), negative_slope=0.2) + x1 = x.max(dim=-1, keepdim=True)[0] + + x = get_graph_feature(x1, device=device) + x = F.leaky_relu(self.bn2(self.conv2(x)), negative_slope=0.2) + x2 = x.max(dim=-1, keepdim=True)[0] + + x = get_graph_feature(x2, device=device) + x = F.leaky_relu(self.bn3(self.conv3(x)), negative_slope=0.2) + x3 = x.max(dim=-1, keepdim=True)[0] + + x = get_graph_feature(x3, device=device) + x = F.leaky_relu(self.bn4(self.conv4(x)), negative_slope=0.2) + x4 = x.max(dim=-1, keepdim=True)[0] + + x = torch.cat((x1, x2, x3, x4), dim=1) + + x = F.leaky_relu(self.bn5(self.conv5(x)), negative_slope=0.2).view(batch_size, -1, num_points) + return x + + +class MLPHead(nn.Module): + def __init__(self, emb_dims): + super(MLPHead, self).__init__() + n_emb_dims = emb_dims + self.n_emb_dims = n_emb_dims + self.nn = nn.Sequential(nn.Linear(n_emb_dims*2, n_emb_dims//2), + nn.BatchNorm1d(n_emb_dims//2), + nn.ReLU(), + nn.Linear(n_emb_dims//2, n_emb_dims//4), + nn.BatchNorm1d(n_emb_dims//4), + nn.ReLU(), + nn.Linear(n_emb_dims//4, n_emb_dims//8), + nn.BatchNorm1d(n_emb_dims//8), + nn.ReLU()) + self.proj_rot = nn.Linear(n_emb_dims//8, 4) + self.proj_trans = nn.Linear(n_emb_dims//8, 3) + + def forward(self, *input): + src_embedding = input[0] + tgt_embedding = input[1] + embedding = torch.cat((src_embedding, tgt_embedding), dim=1) + embedding = self.nn(embedding.max(dim=-1)[0]) + rotation = self.proj_rot(embedding) + rotation = rotation / torch.norm(rotation, p=2, dim=1, keepdim=True) + translation = self.proj_trans(embedding) + return quat2mat(rotation), translation + + +class TemperatureNet(nn.Module): + def __init__(self, emb_dims, temp_factor): + super(TemperatureNet, self).__init__() + self.n_emb_dims = emb_dims + self.temp_factor = temp_factor + self.nn = nn.Sequential(nn.Linear(self.n_emb_dims, 128), + nn.BatchNorm1d(128), + nn.ReLU(), + nn.Linear(128, 128), + nn.BatchNorm1d(128), + nn.ReLU(), + nn.Linear(128, 128), + nn.BatchNorm1d(128), + nn.ReLU(), + nn.Linear(128, 1), + nn.ReLU()) + self.feature_disparity = None + + def forward(self, *input): + src_embedding = input[0] + tgt_embedding = input[1] + src_embedding = src_embedding.mean(dim=2) + tgt_embedding = tgt_embedding.mean(dim=2) + residual = torch.abs(src_embedding-tgt_embedding) + + self.feature_disparity = residual + + return torch.clamp(self.nn(residual), 1.0/self.temp_factor, 1.0*self.temp_factor), residual + + +class SVDHead(nn.Module): + def __init__(self, emb_dims, cat_sampler): + super(SVDHead, self).__init__() + self.n_emb_dims = emb_dims + self.cat_sampler = cat_sampler + self.reflect = nn.Parameter(torch.eye(3), requires_grad=False) + self.reflect[2, 2] = -1 + self.temperature = nn.Parameter(torch.ones(1)*0.5, requires_grad=True) + self.my_iter = torch.ones(1) + + def forward(self, *input): + src_embedding = input[0] + tgt_embedding = input[1] + src = input[2] + tgt = input[3] + batch_size, num_dims, num_points = src.size() + temperature = input[4].view(batch_size, 1, 1) + + if self.cat_sampler == 'softmax': + d_k = src_embedding.size(1) + scores = torch.matmul(src_embedding.transpose(2, 1).contiguous(), tgt_embedding) / math.sqrt(d_k) + scores = torch.softmax(temperature*scores, dim=2) + elif self.cat_sampler == 'gumbel_softmax': + d_k = src_embedding.size(1) + scores = torch.matmul(src_embedding.transpose(2, 1).contiguous(), tgt_embedding) / math.sqrt(d_k) + scores = scores.view(batch_size*num_points, num_points) + temperature = temperature.repeat(1, num_points, 1).view(-1, 1) + scores = F.gumbel_softmax(scores, tau=temperature, hard=True) + scores = scores.view(batch_size, num_points, num_points) + else: + raise Exception('not implemented') + + src_corr = torch.matmul(tgt, scores.transpose(2, 1).contiguous()) + + src_centered = src - src.mean(dim=2, keepdim=True) + + src_corr_centered = src_corr - src_corr.mean(dim=2, keepdim=True) + + H = torch.matmul(src_centered, src_corr_centered.transpose(2, 1).contiguous()).cpu() + + R = [] + + for i in range(src.size(0)): + u, s, v = torch.svd(H[i]) + r = torch.matmul(v, u.transpose(1, 0)).contiguous() + r_det = torch.det(r).item() + diag = torch.from_numpy(np.array([[1.0, 0, 0], + [0, 1.0, 0], + [0, 0, r_det]]).astype('float32')).to(v.device) + r = torch.matmul(torch.matmul(v, diag), u.transpose(1, 0)).contiguous() + R.append(r) + + R = torch.stack(R, dim=0).to(device) + + t = torch.matmul(-R, src.mean(dim=2, keepdim=True)) + src_corr.mean(dim=2, keepdim=True) + if self.training: + self.my_iter += 1 + return R, t.view(batch_size, 3) + + +class KeyPointNet(nn.Module): + def __init__(self, num_keypoints): + super(KeyPointNet, self).__init__() + self.num_keypoints = num_keypoints + + def forward(self, *input): + src = input[0] + tgt = input[1] + src_embedding = input[2] + tgt_embedding = input[3] + batch_size, num_dims, num_points = src_embedding.size() + src_norm = torch.norm(src_embedding, dim=1, keepdim=True) + tgt_norm = torch.norm(tgt_embedding, dim=1, keepdim=True) + src_topk_idx = torch.topk(src_norm, k=self.num_keypoints, dim=2, sorted=False)[1] + tgt_topk_idx = torch.topk(tgt_norm, k=self.num_keypoints, dim=2, sorted=False)[1] + src_keypoints_idx = src_topk_idx.repeat(1, 3, 1) + tgt_keypoints_idx = tgt_topk_idx.repeat(1, 3, 1) + src_embedding_idx = src_topk_idx.repeat(1, num_dims, 1) + tgt_embedding_idx = tgt_topk_idx.repeat(1, num_dims, 1) + + src_keypoints = torch.gather(src, dim=2, index=src_keypoints_idx) + tgt_keypoints = torch.gather(tgt, dim=2, index=tgt_keypoints_idx) + + src_embedding = torch.gather(src_embedding, dim=2, index=src_embedding_idx) + tgt_embedding = torch.gather(tgt_embedding, dim=2, index=tgt_embedding_idx) + return src_keypoints, tgt_keypoints, src_embedding, tgt_embedding + + +class PRNet(nn.Module): + def __init__(self, emb_nn='dgcnn', attention='transformer', head='svd', emb_dims=512, num_keypoints=512, num_subsampled_points=768, num_iters=3, cycle_consistency_loss=0.1, feature_alignment_loss=0.1, discount_factor = 0.9, input_shape='bnc'): + super(PRNet, self).__init__() + self.emb_dims = emb_dims + self.num_keypoints = num_keypoints + self.num_subsampled_points = num_subsampled_points + self.num_iters = num_iters + self.discount_factor = discount_factor + self.feature_alignment_loss = feature_alignment_loss + self.cycle_consistency_loss = cycle_consistency_loss + self.input_shape = input_shape + + if emb_nn == 'pointnet': + self.emb_nn = PointNet(emb_dims=self.emb_dims) + elif emb_nn == 'dgcnn': + self.emb_nn = DGCNN(emb_dims=self.emb_dims) + else: + raise Exception('Not implemented') + + if attention == 'identity': + self.attention = Identity() + elif attention == 'transformer': + self.attention = Transformer(emb_dims=self.emb_dims, n_blocks=1, dropout=0.0, ff_dims=1024, n_heads=4) + else: + raise Exception("Not implemented") + + self.temp_net = TemperatureNet(emb_dims=self.emb_dims, temp_factor=100) + + if head == 'mlp': + self.head = MLPHead(emb_dims=self.emb_dims) + elif head == 'svd': + self.head = SVDHead(emb_dims=self.emb_dims, cat_sampler='softmax') + else: + raise Exception('Not implemented') + + if self.num_keypoints != self.num_subsampled_points: + self.keypointnet = KeyPointNet(num_keypoints=self.num_keypoints) + else: + self.keypointnet = Identity() + + def predict_embedding(self, *input): + src = input[0] + tgt = input[1] + src_embedding = self.emb_nn(src) + tgt_embedding = self.emb_nn(tgt) + + src_embedding_p, tgt_embedding_p = self.attention(src_embedding, tgt_embedding) + + src_embedding = src_embedding + src_embedding_p + tgt_embedding = tgt_embedding + tgt_embedding_p + + src, tgt, src_embedding, tgt_embedding = self.keypointnet(src, tgt, src_embedding, tgt_embedding) + + temperature, feature_disparity = self.temp_net(src_embedding, tgt_embedding) + + return src, tgt, src_embedding, tgt_embedding, temperature, feature_disparity + + # Single Pass Alignment Module for PRNet + def spam(self, *input): + src, tgt, src_embedding, tgt_embedding, temperature, feature_disparity = self.predict_embedding(*input) + rotation_ab, translation_ab = self.head(src_embedding, tgt_embedding, src, tgt, temperature) + rotation_ba, translation_ba = self.head(tgt_embedding, src_embedding, tgt, src, temperature) + return rotation_ab, translation_ab, rotation_ba, translation_ba, feature_disparity + + def predict_keypoint_correspondence(self, *input): + src, tgt, src_embedding, tgt_embedding, temperature, _ = self.predict_embedding(*input) + batch_size, num_dims, num_points = src.size() + d_k = src_embedding.size(1) + scores = torch.matmul(src_embedding.transpose(2, 1).contiguous(), tgt_embedding) / math.sqrt(d_k) + scores = scores.view(batch_size*num_points, num_points) + temperature = temperature.repeat(1, num_points, 1).view(-1, 1) + scores = F.gumbel_softmax(scores, tau=temperature, hard=True) + scores = scores.view(batch_size, num_points, num_points) + return src, tgt, scores + + def forward(self, *input): + calculate_loss = False + if len(input) == 2: + src, tgt = input[0], input[1] + elif len(input) == 3: + src, tgt, rotation_ab, translation_ab = input[0], input[1], input[2][:, :3, :3], input[2][:, :3, 3].view(-1, 3) + calculate_loss = True + elif len(input) == 4: + src, tgt, rotation_ab, translation_ab = input[0], input[1], input[2], input[3] + calculate_loss = True + + if self.input_shape == 'bnc': + src, tgt = src.permute(0, 2, 1), tgt.permute(0, 2, 1) + + batch_size = src.size(0) + identity = torch.eye(3, device=src.device).unsqueeze(0).repeat(batch_size, 1, 1) + + rotation_ab_pred = torch.eye(3, device=src.device, dtype=torch.float32).view(1, 3, 3).repeat(batch_size, 1, 1) + translation_ab_pred = torch.zeros(3, device=src.device, dtype=torch.float32).view(1, 3).repeat(batch_size, 1) + + rotation_ba_pred = torch.eye(3, device=src.device, dtype=torch.float32).view(1, 3, 3).repeat(batch_size, 1, 1) + translation_ba_pred = torch.zeros(3, device=src.device, dtype=torch.float32).view(1, 3).repeat(batch_size, 1) + + total_loss = 0 + total_feature_alignment_loss = 0 + total_cycle_consistency_loss = 0 + total_scale_consensus_loss = 0 + + for i in range(self.num_iters): + rotation_ab_pred_i, translation_ab_pred_i, rotation_ba_pred_i, translation_ba_pred_i, feature_disparity = self.spam(src, tgt) + + rotation_ab_pred = torch.matmul(rotation_ab_pred_i, rotation_ab_pred) + translation_ab_pred = torch.matmul(rotation_ab_pred_i, translation_ab_pred.unsqueeze(2)).squeeze(2) + translation_ab_pred_i + + rotation_ba_pred = torch.matmul(rotation_ba_pred_i, rotation_ba_pred) + translation_ba_pred = torch.matmul(rotation_ba_pred_i, translation_ba_pred.unsqueeze(2)).squeeze(2) + translation_ba_pred_i + + if calculate_loss: + loss = (F.mse_loss(torch.matmul(rotation_ab_pred.transpose(2, 1), rotation_ab), identity) \ + + F.mse_loss(translation_ab_pred, translation_ab)) * self.discount_factor**i + + feature_alignment_loss = feature_disparity.mean() * self.feature_alignment_loss * self.discount_factor**i + cycle_consistency_loss = cycle_consistency(rotation_ab_pred_i, translation_ab_pred_i, + rotation_ba_pred_i, translation_ba_pred_i) \ + * self.cycle_consistency_loss * self.discount_factor**i + + scale_consensus_loss = 0 + total_feature_alignment_loss += feature_alignment_loss + total_cycle_consistency_loss += cycle_consistency_loss + total_loss = total_loss + loss + feature_alignment_loss + cycle_consistency_loss + scale_consensus_loss + + if self.input_shape == 'bnc': + src = transform.transform_point_cloud(src.permute(0, 2, 1), rotation_ab_pred_i, translation_ab_pred_i).permute(0, 2, 1) + else: + src = transform.transform_point_cloud(src, rotation_ab_pred_i, translation_ab_pred_i) + + if self.input_shape == 'bnc': + src, tgt = src.permute(0, 2, 1), tgt.permute(0, 2, 1) + + result = {'est_R': rotation_ab_pred, + 'est_t': translation_ab_pred, + 'est_T': transform.convert2transformation(rotation_ab_pred, translation_ab_pred), + 'transformed_source': src} + + if calculate_loss: + result['loss'] = total_loss + return result + + +if __name__ == '__main__': + model = PRNet() + src = torch.tensor(10, 1024, 3) + tgt = torch.tensor(10, 768, 3) + rotation_ab, translation_ab = torch.tensor(10, 3, 3), torch.tensor(10, 3) + src, tgt = src.to(device), tgt.to(device) + rotation_ab, translation_ab = rotation_ab.to(device), translation_ab.to(device) + rotation_ab_pred, translation_ab_pred, loss = model(src, tgt, rotation_ab, translation_ab) \ No newline at end of file diff --git a/thirdparty/learning3d/models/r3pmnet.py b/thirdparty/learning3d/models/r3pmnet.py new file mode 100644 index 0000000000000000000000000000000000000000..64ad549a7af64bd5406887fca028a23a648fcba4 --- /dev/null +++ b/thirdparty/learning3d/models/r3pmnet.py @@ -0,0 +1,396 @@ +''' +modified script to use PointNet instead of PPFNet for feature extraction +put it in the miniconda environment for the changes to take effect +''' +import argparse +import logging +import numpy as np +import torch +import torch.nn as nn +import torch.nn.functional as F + +from .. utils import square_distance, angle_difference +from .. ops.transform_functions import convert2transformation +from .pointnet import PointNet + +_EPS = 1e-5 # To prevent division by zero + +class ParameterPredictionNet(nn.Module): + def __init__(self, weights_dim): + """PointNet based Parameter prediction network + + Args: + weights_dim: Number of weights to predict (excluding beta), should be something like + [3], or [64, 3], for 3 types of features + """ + + super().__init__() + + self._logger = logging.getLogger(self.__class__.__name__) + + self.weights_dim = weights_dim + + # Pointnet + self.prepool = nn.Sequential( + nn.Conv1d(4, 64, 1), + nn.GroupNorm(8, 64), + nn.ReLU(), + + nn.Conv1d(64, 64, 1), + nn.GroupNorm(8, 64), + nn.ReLU(), + + nn.Conv1d(64, 64, 1), + nn.GroupNorm(8, 64), + nn.ReLU(), + + nn.Conv1d(64, 128, 1), + nn.GroupNorm(8, 128), + nn.ReLU(), + + nn.Conv1d(128, 1024, 1), + nn.GroupNorm(16, 1024), + nn.ReLU(), + ) + self.pooling = nn.AdaptiveMaxPool1d(1) + self.postpool = nn.Sequential( + nn.Linear(1024, 512), + nn.GroupNorm(16, 512), + nn.ReLU(), + + nn.Linear(512, 256), + nn.GroupNorm(16, 256), + nn.ReLU(), + + nn.Linear(256, 2 + np.prod(weights_dim)), + ) + + self._logger.info('Predicting weights with dim {}.'.format(self.weights_dim)) + + def forward(self, x): + """ Returns alpha, beta, and gating_weights (if needed) + + Args: + x: List containing two point clouds, x[0] = src (B, J, 3), x[1] = ref (B, K, 3) + + Returns: + beta, alpha, weightings + """ + # X and Y concatenated + src_padded = F.pad(x[0], (0, 1), mode='constant', value=0) + ref_padded = F.pad(x[1], (0, 1), mode='constant', value=1) + concatenated = torch.cat([src_padded, ref_padded], dim=1) + + prepool_feat = self.prepool(concatenated.permute(0, 2, 1)) + pooled = torch.flatten(self.pooling(prepool_feat), start_dim=-2) + raw_weights = self.postpool(pooled) + + # softplus to ensure positivity + beta = F.softplus(raw_weights[:, 0]) + alpha = F.softplus(raw_weights[:, 1]) + + return beta, alpha + + + +def to_numpy(tensor): + """Wrapper around .detach().cpu().numpy() """ + if isinstance(tensor, torch.Tensor): + return tensor.detach().cpu().numpy() + elif isinstance(tensor, np.ndarray): + return tensor + else: + raise NotImplementedError + + +def se3_transform(g, a, normals=None): + """ Applies the SE3 transform + + Args: + g: SE3 transformation matrix of size ([1,] 3/4, 4) or (B, 3/4, 4) + a: Points to be transformed (N, 3) or (B, N, 3) + normals: (Optional). If provided, normals will be transformed + + Returns: + transformed points of size (N, 3) or (B, N, 3) + + """ + R = g[..., :3, :3] # (B, 3, 3) + p = g[..., :3, 3] # (B, 3) + + if len(g.size()) == len(a.size()): + b = torch.matmul(a, R.transpose(-1, -2)) + p[..., None, :] + else: + raise NotImplementedError + b = R.matmul(a.unsqueeze(-1)).squeeze(-1) + p # No batch. Not checked + + if normals is not None: + rotated_normals = normals @ R.transpose(-1, -2) + return b, rotated_normals + + else: + return b + +def match_features(feat_src, feat_ref, metric='l2'): + """ Compute pairwise distance between features + + Args: + feat_src: (B, J, C) + feat_ref: (B, K, C) + metric: either 'angle' or 'l2' (squared euclidean) + + Returns: + Matching matrix (B, J, K). i'th row describes how well the i'th point + in the src agrees with every point in the ref. + """ + if feat_src.shape[-1] != feat_ref.shape[-1]: + if feat_src.shape[-1] > feat_ref.shape[-1]: + feat_src = feat_src[:,:,:feat_ref.shape[-1]] + elif feat_src.shape[-1] < feat_ref.shape[-1]: + feat_ref = feat_ref[:,:,:feat_src.shape[-1]] + + assert feat_src.shape[-1] == feat_ref.shape[-1] + + if metric == 'l2': + dist_matrix = square_distance(feat_src, feat_ref) + elif metric == 'angle': + feat_src_norm = feat_src / (torch.norm(feat_src, dim=-1, keepdim=True) + _EPS) + feat_ref_norm = feat_ref / (torch.norm(feat_ref, dim=-1, keepdim=True) + _EPS) + + dist_matrix = angle_difference(feat_src_norm, feat_ref_norm) + else: + raise NotImplementedError + + return dist_matrix + + +def sinkhorn(log_alpha, n_iters: int = 5, slack: bool = True, eps: float = -1) -> torch.Tensor: + """ Run sinkhorn iterations to generate a near doubly stochastic matrix, where each row or column sum to <=1 + + Args: + log_alpha: log of positive matrix to apply sinkhorn normalization (B, J, K) + n_iters (int): Number of normalization iterations + slack (bool): Whether to include slack row and column + eps: eps for early termination (Used only for handcrafted RPM). Set to negative to disable. + + Returns: + log(perm_matrix): Doubly stochastic matrix (B, J, K) + + Modified from original source taken from: + Learning Latent Permutations with Gumbel-Sinkhorn Networks + https://github.com/HeddaCohenIndelman/Learning-Gumbel-Sinkhorn-Permutations-w-Pytorch + """ + + # Sinkhorn iterations + prev_alpha = None + if slack: + zero_pad = nn.ZeroPad2d((0, 1, 0, 1)) + log_alpha_padded = zero_pad(log_alpha[:, None, :, :]) + + log_alpha_padded = torch.squeeze(log_alpha_padded, dim=1) + + for i in range(n_iters): + # Row normalization + log_alpha_padded = torch.cat(( + log_alpha_padded[:, :-1, :] - (torch.logsumexp(log_alpha_padded[:, :-1, :], dim=2, keepdim=True)), + log_alpha_padded[:, -1, None, :]), # Don't normalize last row + dim=1) + + # Column normalization + log_alpha_padded = torch.cat(( + log_alpha_padded[:, :, :-1] - (torch.logsumexp(log_alpha_padded[:, :, :-1], dim=1, keepdim=True)), + log_alpha_padded[:, :, -1, None]), # Don't normalize last column + dim=2) + + if eps > 0: + if prev_alpha is not None: + abs_dev = torch.abs(torch.exp(log_alpha_padded[:, :-1, :-1]) - prev_alpha) + if torch.max(torch.sum(abs_dev, dim=[1, 2])) < eps: + break + prev_alpha = torch.exp(log_alpha_padded[:, :-1, :-1]).clone() + + log_alpha = log_alpha_padded[:, :-1, :-1] + else: + for i in range(n_iters): + # Row normalization (i.e. each row sum to 1) + log_alpha = log_alpha - (torch.logsumexp(log_alpha, dim=2, keepdim=True)) + + # Column normalization (i.e. each column sum to 1) + log_alpha = log_alpha - (torch.logsumexp(log_alpha, dim=1, keepdim=True)) + + if eps > 0: + if prev_alpha is not None: + abs_dev = torch.abs(torch.exp(log_alpha) - prev_alpha) + if torch.max(torch.sum(abs_dev, dim=[1, 2])) < eps: + break + prev_alpha = torch.exp(log_alpha).clone() + + return log_alpha + + +def compute_rigid_transform(a: torch.Tensor, b: torch.Tensor, weights: torch.Tensor): + """Compute rigid transforms between two point sets + + Args: + a (torch.Tensor): (B, M, 3) points + b (torch.Tensor): (B, N, 3) points + weights (torch.Tensor): (B, M) + + Returns: + Transform T (B, 3, 4) to get from a to b, i.e. T*a = b + """ + + weights_normalized = weights[..., None] / (torch.sum(weights[..., None], dim=1, keepdim=True) + _EPS) + centroid_a = torch.sum(a * weights_normalized, dim=1) + centroid_b = torch.sum(b * weights_normalized, dim=1) + a_centered = a - centroid_a[:, None, :] + b_centered = b - centroid_b[:, None, :] + cov = a_centered.transpose(-2, -1) @ (b_centered * weights_normalized) + + # Compute rotation using Kabsch algorithm. Will compute two copies with +/-V[:,:3] + # and choose based on determinant to avoid flips + u, s, v = torch.svd(cov, some=False, compute_uv=True) + rot_mat_pos = v @ u.transpose(-1, -2) + v_neg = v.clone() + v_neg[:, :, 2] *= -1 + rot_mat_neg = v_neg @ u.transpose(-1, -2) + rot_mat = torch.where(torch.det(rot_mat_pos)[:, None, None] > 0, rot_mat_pos, rot_mat_neg) + assert torch.all(torch.det(rot_mat) > 0) + + # Compute translation (uncenter centroid) + translation = -rot_mat @ centroid_a[:, :, None] + centroid_b[:, :, None] + + transform = torch.cat((rot_mat, translation), dim=2) + return transform + + +class RPMNet(nn.Module): + def __init__(self, feature_model=PointNet()): + super().__init__() + + self.add_slack = True + self.num_sk_iter = 5 + + self.weights_net = ParameterPredictionNet(weights_dim=[0]) + self.feat_extractor = feature_model + + def compute_affinity(self, beta, feat_distance, alpha=0.5): + """Compute logarithm of Initial match matrix values, i.e. log(m_jk)""" + if isinstance(alpha, float): + hybrid_affinity = -beta[:, None, None] * (feat_distance - alpha) + else: + hybrid_affinity = -beta[:, None, None] * (feat_distance - alpha[:, None, None]) + return hybrid_affinity + + @staticmethod + def split_normals(data): + if data.shape[2] == 6: + xyz, normals = data[:, :, :3], data[:, :, 3:6] + elif data.shape[2] == 3: + xyz, normals = data, torch.zeros(data.shape).to(data.device) + return xyz, normals + + def spam(self, xyz_template, norm_template, xyz_source, norm_source): + self.beta, self.alpha = self.weights_net([xyz_source, xyz_template]) + + try: + self.feat_source = self.feat_extractor(xyz_source, norm_source) + self.feat_template = self.feat_extractor(xyz_template, norm_template) + except: + try: # if feature extractor is PointNet + self.feat_source = self.feat_extractor(xyz_source) + self.feat_template = self.feat_extractor(xyz_template) + except: # if feature extractor is FCGF + # reshape the data + xyz_source_reshaped = xyz_source.permute(0, 2, 1).unsqueeze(3).unsqueeze(4) + xyz_template_reshaped = xyz_template.permute(0, 2, 1).unsqueeze(3).unsqueeze(4) + self.feat_source = self.feat_extractor(xyz_source_reshaped) + self.feat_template = self.feat_extractor(xyz_template_reshaped) + # # reshape the features back to (B, N, C) + self.feat_source = self.feat_source.squeeze(-1).squeeze(-1).permute(0, 2, 1) + self.feat_template = self.feat_template.squeeze(-1).squeeze(-1).permute(0, 2, 1) + + feat_distance = match_features(self.feat_source, self.feat_template) + self.affinity = self.compute_affinity(self.beta, feat_distance, alpha=self.alpha) + + # Compute weighted coordinates + log_perm_matrix = sinkhorn(self.affinity, n_iters=self.num_sk_iter, slack=self.add_slack) + self.perm_matrix = torch.exp(log_perm_matrix) + try: + weighted_template = self.perm_matrix @ xyz_template / (torch.sum(self.perm_matrix, dim=2, keepdim=True) + _EPS) + except: # if feature extractor is PointNet + weighted_template = self.perm_matrix @ xyz_template[:,:self.perm_matrix.shape[1]] / (torch.sum(self.perm_matrix, dim=2, keepdim=True) + _EPS) + return weighted_template + + def forward(self, template, source, max_iterations: int = 1): + """Forward pass for RPMNet + + Args: + data: Dict containing the following fields: + 'points_src': Source points (B, J, 6) + 'points_ref': Reference points (B, K, 6) + num_iter (int): Number of iterations. Recommended to be 2 for training + + Returns: + transform: Transform to apply to source points such that they align to reference + src_transformed: Transformed source points + """ + + xyz_template, norm_template = self.split_normals(template) + xyz_source, norm_source = self.split_normals(source) + + xyz_source_t, norm_source_t = xyz_source, norm_source # a copy of source to apply transformation to + + transforms = [] + all_gamma, all_perm_matrices, all_weighted_template = [], [], [] + all_beta, all_alpha = [], [] + + for i in range(max_iterations): + weighted_template = self.spam(xyz_template, norm_template, xyz_source_t, norm_source_t) # Finding better correspondences after each iteration. + + # Compute transform and transform points + try: + transform = compute_rigid_transform(xyz_source_t, weighted_template, weights=torch.sum(self.perm_matrix, dim=2)) + xyz_source_t, norm_source_t = se3_transform(transform.detach(), xyz_source, norm_source) # Apply transformation to original source. + except: # if feature extractor is PointNet + transform = compute_rigid_transform(xyz_source[:,:weighted_template.shape[1]], weighted_template, weights=torch.sum(self.perm_matrix, dim=2)) + xyz_source_t, norm_source_t = se3_transform(transform.detach(), xyz_source[:,:weighted_template.shape[1]], norm_source) # Apply transformation to original source. + + transforms.append(transform) + all_gamma.append(torch.exp(self.affinity)) + all_perm_matrices.append(self.perm_matrix) + all_weighted_template.append(weighted_template) + all_beta.append(to_numpy(self.beta)) + all_alpha.append(to_numpy(self.alpha)) + + est_T = convert2transformation(transforms[max_iterations-1][:, :3, :3], transforms[max_iterations-1][:, :3, 3]) + transformed_source = torch.bmm(est_T[:, :3, :3], source[:,:,:3].permute(0, 2, 1)).permute(0, 2, 1) + est_T[:, :3, 3].unsqueeze(1) + + try: # for training + result = {'est_R': est_T[:, :3, :3], # source -> template + 'est_t': est_T[:, :3, 3], # source -> template + 'est_T': est_T, # source -> template + 'r': self.feat_template - self.feat_source, + 'transformed_source': transformed_source} + except RuntimeError: + result = {'est_R': est_T[:, :3, :3], # source -> template + 'est_t': est_T[:, :3, 3], # source -> template + 'est_T': est_T, # source -> template + 'transformed_source': transformed_source} + + result['perm_matrices_init'] = all_gamma + result['perm_matrices'] = all_perm_matrices + result['weighted_template'] = all_weighted_template + result['beta'] = np.stack(all_beta, axis=0) + result['alpha'] = np.stack(all_alpha, axis=0) + result['transforms'] = transforms + + return result + + +if __name__ == '__main__': + template, source = torch.rand(10,1024,6), torch.rand(10,1024,6) + + net = RPMNet() + result = net(template, source) + import ipdb; ipdb.set_trace() \ No newline at end of file diff --git a/thirdparty/learning3d/models/rpmnet.py b/thirdparty/learning3d/models/rpmnet.py new file mode 100644 index 0000000000000000000000000000000000000000..467cf298a230ee97ecda1db268f97a5c27203a43 --- /dev/null +++ b/thirdparty/learning3d/models/rpmnet.py @@ -0,0 +1,359 @@ +import argparse +import logging +import numpy as np +import torch +import torch.nn as nn +import torch.nn.functional as F + +from .. utils import square_distance, angle_difference +from .. ops.transform_functions import convert2transformation +from .ppfnet import PPFNet +_EPS = 1e-5 # To prevent division by zero + + +class ParameterPredictionNet(nn.Module): + def __init__(self, weights_dim): + """PointNet based Parameter prediction network + + Args: + weights_dim: Number of weights to predict (excluding beta), should be something like + [3], or [64, 3], for 3 types of features + """ + + super().__init__() + + self._logger = logging.getLogger(self.__class__.__name__) + + self.weights_dim = weights_dim + + # Pointnet + self.prepool = nn.Sequential( + nn.Conv1d(4, 64, 1), + nn.GroupNorm(8, 64), + nn.ReLU(), + + nn.Conv1d(64, 64, 1), + nn.GroupNorm(8, 64), + nn.ReLU(), + + nn.Conv1d(64, 64, 1), + nn.GroupNorm(8, 64), + nn.ReLU(), + + nn.Conv1d(64, 128, 1), + nn.GroupNorm(8, 128), + nn.ReLU(), + + nn.Conv1d(128, 1024, 1), + nn.GroupNorm(16, 1024), + nn.ReLU(), + ) + self.pooling = nn.AdaptiveMaxPool1d(1) + self.postpool = nn.Sequential( + nn.Linear(1024, 512), + nn.GroupNorm(16, 512), + nn.ReLU(), + + nn.Linear(512, 256), + nn.GroupNorm(16, 256), + nn.ReLU(), + + nn.Linear(256, 2 + np.prod(weights_dim)), + ) + + self._logger.info('Predicting weights with dim {}.'.format(self.weights_dim)) + + def forward(self, x): + """ Returns alpha, beta, and gating_weights (if needed) + + Args: + x: List containing two point clouds, x[0] = src (B, J, 3), x[1] = ref (B, K, 3) + + Returns: + beta, alpha, weightings + """ + + src_padded = F.pad(x[0], (0, 1), mode='constant', value=0) + ref_padded = F.pad(x[1], (0, 1), mode='constant', value=1) + concatenated = torch.cat([src_padded, ref_padded], dim=1) + + prepool_feat = self.prepool(concatenated.permute(0, 2, 1)) + pooled = torch.flatten(self.pooling(prepool_feat), start_dim=-2) + raw_weights = self.postpool(pooled) + + beta = F.softplus(raw_weights[:, 0]) + alpha = F.softplus(raw_weights[:, 1]) + + return beta, alpha + + + +def to_numpy(tensor): + """Wrapper around .detach().cpu().numpy() """ + if isinstance(tensor, torch.Tensor): + return tensor.detach().cpu().numpy() + elif isinstance(tensor, np.ndarray): + return tensor + else: + raise NotImplementedError + + +def se3_transform(g, a, normals=None): + """ Applies the SE3 transform + + Args: + g: SE3 transformation matrix of size ([1,] 3/4, 4) or (B, 3/4, 4) + a: Points to be transformed (N, 3) or (B, N, 3) + normals: (Optional). If provided, normals will be transformed + + Returns: + transformed points of size (N, 3) or (B, N, 3) + + """ + R = g[..., :3, :3] # (B, 3, 3) + p = g[..., :3, 3] # (B, 3) + + if len(g.size()) == len(a.size()): + b = torch.matmul(a, R.transpose(-1, -2)) + p[..., None, :] + else: + raise NotImplementedError + b = R.matmul(a.unsqueeze(-1)).squeeze(-1) + p # No batch. Not checked + + if normals is not None: + rotated_normals = normals @ R.transpose(-1, -2) + return b, rotated_normals + + else: + return b + + +def match_features(feat_src, feat_ref, metric='l2'): + """ Compute pairwise distance between features + + Args: + feat_src: (B, J, C) + feat_ref: (B, K, C) + metric: either 'angle' or 'l2' (squared euclidean) + + Returns: + Matching matrix (B, J, K). i'th row describes how well the i'th point + in the src agrees with every point in the ref. + """ + assert feat_src.shape[-1] == feat_ref.shape[-1] + + if metric == 'l2': + dist_matrix = square_distance(feat_src, feat_ref) + elif metric == 'angle': + feat_src_norm = feat_src / (torch.norm(feat_src, dim=-1, keepdim=True) + _EPS) + feat_ref_norm = feat_ref / (torch.norm(feat_ref, dim=-1, keepdim=True) + _EPS) + + dist_matrix = angle_difference(feat_src_norm, feat_ref_norm) + else: + raise NotImplementedError + + return dist_matrix + + +def sinkhorn(log_alpha, n_iters: int = 5, slack: bool = True, eps: float = -1) -> torch.Tensor: + """ Run sinkhorn iterations to generate a near doubly stochastic matrix, where each row or column sum to <=1 + + Args: + log_alpha: log of positive matrix to apply sinkhorn normalization (B, J, K) + n_iters (int): Number of normalization iterations + slack (bool): Whether to include slack row and column + eps: eps for early termination (Used only for handcrafted RPM). Set to negative to disable. + + Returns: + log(perm_matrix): Doubly stochastic matrix (B, J, K) + + Modified from original source taken from: + Learning Latent Permutations with Gumbel-Sinkhorn Networks + https://github.com/HeddaCohenIndelman/Learning-Gumbel-Sinkhorn-Permutations-w-Pytorch + """ + + # Sinkhorn iterations + prev_alpha = None + if slack: + zero_pad = nn.ZeroPad2d((0, 1, 0, 1)) + log_alpha_padded = zero_pad(log_alpha[:, None, :, :]) + + log_alpha_padded = torch.squeeze(log_alpha_padded, dim=1) + + for i in range(n_iters): + # Row normalization + log_alpha_padded = torch.cat(( + log_alpha_padded[:, :-1, :] - (torch.logsumexp(log_alpha_padded[:, :-1, :], dim=2, keepdim=True)), + log_alpha_padded[:, -1, None, :]), # Don't normalize last row + dim=1) + + # Column normalization + log_alpha_padded = torch.cat(( + log_alpha_padded[:, :, :-1] - (torch.logsumexp(log_alpha_padded[:, :, :-1], dim=1, keepdim=True)), + log_alpha_padded[:, :, -1, None]), # Don't normalize last column + dim=2) + + if eps > 0: + if prev_alpha is not None: + abs_dev = torch.abs(torch.exp(log_alpha_padded[:, :-1, :-1]) - prev_alpha) + if torch.max(torch.sum(abs_dev, dim=[1, 2])) < eps: + break + prev_alpha = torch.exp(log_alpha_padded[:, :-1, :-1]).clone() + + log_alpha = log_alpha_padded[:, :-1, :-1] + else: + for i in range(n_iters): + # Row normalization (i.e. each row sum to 1) + log_alpha = log_alpha - (torch.logsumexp(log_alpha, dim=2, keepdim=True)) + + # Column normalization (i.e. each column sum to 1) + log_alpha = log_alpha - (torch.logsumexp(log_alpha, dim=1, keepdim=True)) + + if eps > 0: + if prev_alpha is not None: + abs_dev = torch.abs(torch.exp(log_alpha) - prev_alpha) + if torch.max(torch.sum(abs_dev, dim=[1, 2])) < eps: + break + prev_alpha = torch.exp(log_alpha).clone() + + return log_alpha + + +def compute_rigid_transform(a: torch.Tensor, b: torch.Tensor, weights: torch.Tensor): + """Compute rigid transforms between two point sets + + Args: + a (torch.Tensor): (B, M, 3) points + b (torch.Tensor): (B, N, 3) points + weights (torch.Tensor): (B, M) + + Returns: + Transform T (B, 3, 4) to get from a to b, i.e. T*a = b + """ + + weights_normalized = weights[..., None] / (torch.sum(weights[..., None], dim=1, keepdim=True) + _EPS) + centroid_a = torch.sum(a * weights_normalized, dim=1) + centroid_b = torch.sum(b * weights_normalized, dim=1) + a_centered = a - centroid_a[:, None, :] + b_centered = b - centroid_b[:, None, :] + cov = a_centered.transpose(-2, -1) @ (b_centered * weights_normalized) + + # Compute rotation using Kabsch algorithm. Will compute two copies with +/-V[:,:3] + # and choose based on determinant to avoid flips + u, s, v = torch.svd(cov, some=False, compute_uv=True) + rot_mat_pos = v @ u.transpose(-1, -2) + v_neg = v.clone() + v_neg[:, :, 2] *= -1 + rot_mat_neg = v_neg @ u.transpose(-1, -2) + rot_mat = torch.where(torch.det(rot_mat_pos)[:, None, None] > 0, rot_mat_pos, rot_mat_neg) + assert torch.all(torch.det(rot_mat) > 0) + + # Compute translation (uncenter centroid) + translation = -rot_mat @ centroid_a[:, :, None] + centroid_b[:, :, None] + + transform = torch.cat((rot_mat, translation), dim=2) + return transform + + +class RPMNet(nn.Module): + def __init__(self, feature_model=PPFNet()): + super().__init__() + + self.add_slack = True + self.num_sk_iter = 5 + + self.weights_net = ParameterPredictionNet(weights_dim=[0]) + self.feat_extractor = feature_model + + def compute_affinity(self, beta, feat_distance, alpha=0.5): + """Compute logarithm of Initial match matrix values, i.e. log(m_jk)""" + if isinstance(alpha, float): + hybrid_affinity = -beta[:, None, None] * (feat_distance - alpha) + else: + hybrid_affinity = -beta[:, None, None] * (feat_distance - alpha[:, None, None]) + return hybrid_affinity + + @staticmethod + def split_normals(data): + if data.shape[2] == 6: + xyz, normals = data[:, :, :3], data[:, :, 3:6] + elif data.shape[2] == 3: + xyz, normals = data, torch.zeros(data.shape).to(data.device) + return xyz, normals + + def spam(self, xyz_template, norm_template, xyz_source, norm_source): + self.beta, self.alpha = self.weights_net([xyz_source, xyz_template]) + self.feat_source = self.feat_extractor(xyz_source, norm_source) + self.feat_template = self.feat_extractor(xyz_template, norm_template) + + feat_distance = match_features(self.feat_source, self.feat_template) + self.affinity = self.compute_affinity(self.beta, feat_distance, alpha=self.alpha) + + # Compute weighted coordinates + log_perm_matrix = sinkhorn(self.affinity, n_iters=self.num_sk_iter, slack=self.add_slack) + self.perm_matrix = torch.exp(log_perm_matrix) + weighted_template = self.perm_matrix @ xyz_template / (torch.sum(self.perm_matrix, dim=2, keepdim=True) + _EPS) + + return weighted_template + + def forward(self, template, source, max_iterations: int = 1): + """Forward pass for RPMNet + + Args: + data: Dict containing the following fields: + 'points_src': Source points (B, J, 6) + 'points_ref': Reference points (B, K, 6) + num_iter (int): Number of iterations. Recommended to be 2 for training + + Returns: + transform: Transform to apply to source points such that they align to reference + src_transformed: Transformed source points + """ + + xyz_template, norm_template = self.split_normals(template) + xyz_source, norm_source = self.split_normals(source) + + xyz_source_t, norm_source_t = xyz_source, norm_source + + transforms = [] + all_gamma, all_perm_matrices, all_weighted_template = [], [], [] + all_beta, all_alpha = [], [] + + for i in range(max_iterations): + weighted_template = self.spam(xyz_template, norm_template, xyz_source_t, norm_source_t) # Finding better correspondences after each iteration. + + # Compute transform and transform points + transform = compute_rigid_transform(xyz_source, weighted_template, weights=torch.sum(self.perm_matrix, dim=2)) + xyz_source_t, norm_source_t = se3_transform(transform.detach(), xyz_source, norm_source) # Apply transformation to original source. + + transforms.append(transform) + all_gamma.append(torch.exp(self.affinity)) + all_perm_matrices.append(self.perm_matrix) + all_weighted_template.append(weighted_template) + all_beta.append(to_numpy(self.beta)) + all_alpha.append(to_numpy(self.alpha)) + + est_T = convert2transformation(transforms[max_iterations-1][:, :3, :3], transforms[max_iterations-1][:, :3, 3]) + transformed_source = torch.bmm(est_T[:, :3, :3], source[:,:,:3].permute(0, 2, 1)).permute(0, 2, 1) + est_T[:, :3, 3].unsqueeze(1) + + result = {'est_R': est_T[:, :3, :3], # source -> template + 'est_t': est_T[:, :3, 3], # source -> template + 'est_T': est_T, # source -> template + # 'r': self.feat_template - self.feat_source, + 'transformed_source': transformed_source} + + result['perm_matrices_init'] = all_gamma + result['perm_matrices'] = all_perm_matrices + result['weighted_template'] = all_weighted_template + result['beta'] = np.stack(all_beta, axis=0) + result['alpha'] = np.stack(all_alpha, axis=0) + result['transforms'] = transforms + + return result + + +if __name__ == '__main__': + template, source = torch.rand(10,1024,6), torch.rand(10,1024,6) + + net = RPMNet() + result = net(template, source) + import ipdb; ipdb.set_trace() \ No newline at end of file diff --git a/thirdparty/learning3d/models/segmentation.py b/thirdparty/learning3d/models/segmentation.py new file mode 100644 index 0000000000000000000000000000000000000000..25bdd9d599179f01f66ef30f3dab7f00182f0355 --- /dev/null +++ b/thirdparty/learning3d/models/segmentation.py @@ -0,0 +1,38 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F + + +class Segmentation(nn.Module): + def __init__(self, feature_model, num_classes=40): + super(Segmentation, self).__init__() + self.feature_model = feature_model + self.num_classes = num_classes + + self.conv1 = torch.nn.Conv1d(self.feature_model.emb_dims+64, 512, 1) + self.conv2 = torch.nn.Conv1d(512, 256, 1) + self.conv3 = torch.nn.Conv1d(256, 128, 1) + self.conv4 = torch.nn.Conv1d(128, self.num_classes, 1) + self.bn1 = nn.BatchNorm1d(512) + self.bn2 = nn.BatchNorm1d(256) + self.bn3 = nn.BatchNorm1d(128) + + def forward(self, input_data): + output = self.feature_model(input_data) + output = F.relu(self.bn1(self.conv1(output))) + output = F.relu(self.bn2(self.conv2(output))) + output = F.relu(self.bn3(self.conv3(output))) + output = self.conv4(output) + output = output.permute(0, 2, 1) # B x N x num_classes + return output + +if __name__ == '__main__': + from pointnet import PointNet + x = torch.rand(10,1024,3) + + pn = PointNet(global_feat=False) + seg = Segmentation(pn) + seg_result = seg(x) + + print('Input Shape: {}\n Segmentation Output Shape: {}' + .format(x.shape, seg_result.shape)) \ No newline at end of file diff --git a/thirdparty/learning3d/ops/__init__.py b/thirdparty/learning3d/ops/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/thirdparty/learning3d/ops/data_utils.py b/thirdparty/learning3d/ops/data_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..552efbd6a15a9ab36720b073c50485f97c1757c1 --- /dev/null +++ b/thirdparty/learning3d/ops/data_utils.py @@ -0,0 +1,45 @@ +import torch + +def mean_shift(template, source, p0_zero_mean, p1_zero_mean): + template_mean = torch.eye(3).view(1, 3, 3).expand(template.size(0), 3, 3).to(template) # [B, 3, 3] + source_mean = torch.eye(3).view(1, 3, 3).expand(source.size(0), 3, 3).to(source) # [B, 3, 3] + + if p0_zero_mean: + p0_m = template.mean(dim=1) # [B, N, 3] -> [B, 3] + template_mean = torch.cat([template_mean, p0_m.unsqueeze(-1)], dim=2) + one_ = torch.tensor([[[0.0, 0.0, 0.0, 1.0]]]).repeat(template_mean.shape[0], 1, 1).to(template_mean) # (Bx1x4) + template_mean = torch.cat([template_mean, one_], dim=1) + template = template - p0_m.unsqueeze(1) + # else: + # q0 = template + + if p1_zero_mean: + #print(numpy.any(numpy.isnan(p1.numpy()))) + p1_m = source.mean(dim=1) # [B, N, 3] -> [B, 3] + source_mean = torch.cat([source_mean, -p0_m.unsqueeze(-1)], dim=2) + one_ = torch.tensor([[[0.0, 0.0, 0.0, 1.0]]]).repeat(source_mean.shape[0], 1, 1).to(source_mean) # (Bx1x4) + source_mean = torch.cat([source_mean, one_], dim=1) + source = source - p1_m.unsqueeze(1) + # else: + # q1 = source + return template, source, template_mean, source_mean + +def postprocess_data(result, p0, p1, a0, a1, p0_zero_mean, p1_zero_mean): + #output' = trans(p0_m) * output * trans(-p1_m) + # = [I, p0_m;] * [R, t;] * [I, -p1_m;] + # [0, 1 ] [0, 1 ] [0, 1 ] + est_g = result['est_T'] + if p0_zero_mean: + est_g = a0.to(est_g).bmm(est_g) + if p1_zero_mean: + est_g = est_g.bmm(a1.to(est_g)) + result['est_T'] = est_g + + est_gs = result['est_T_series'] # [M, B, 4, 4] + if p0_zero_mean: + est_gs = a0.unsqueeze(0).contiguous().to(est_gs).matmul(est_gs) + if p1_zero_mean: + est_gs = est_gs.matmul(a1.unsqueeze(0).contiguous().to(est_gs)) + result['est_T_series'] = est_gs + + return result diff --git a/thirdparty/learning3d/ops/invmat.py b/thirdparty/learning3d/ops/invmat.py new file mode 100644 index 0000000000000000000000000000000000000000..21e957a6f81731a771d9e0e3831b6e5e54e5ce76 --- /dev/null +++ b/thirdparty/learning3d/ops/invmat.py @@ -0,0 +1,134 @@ +""" inverse matrix """ + +import torch + + +def batch_inverse(x): + """ M(n) -> M(n); x -> x^-1 """ + batch_size, h, w = x.size() + assert h == w + y = torch.zeros_like(x) + for i in range(batch_size): + y[i, :, :] = x[i, :, :].inverse() + return y + +def batch_inverse_dx(y): + """ backward """ + # Let y(x) = x^-1. + # compute dy + # dy = dy(j,k) + # = - y(j,m) * dx(m,n) * y(n,k) + # = - y(j,m) * y(n,k) * dx(m,n) + # therefore, + # dy(j,k)/dx(m,n) = - y(j,m) * y(n,k) + batch_size, h, w = y.size() + assert h == w + # compute dy(j,k,m,n) = dy(j,k)/dx(m,n) = - y(j,m) * y(n,k) + # = - (y(j,:))' * y'(k,:) + yl = y.repeat(1, 1, h).view(batch_size*h*h, h, 1) + yr = y.transpose(1, 2).repeat(1, h, 1).view(batch_size*h*h, 1, h) + dy = - yl.bmm(yr).view(batch_size, h, h, h, h) + + # compute dy(m,n,j,k) = dy(j,k)/dx(m,n) = - y(j,m) * y(n,k) + # = - (y'(m,:))' * y(n,:) + #yl = y.transpose(1, 2).repeat(1, 1, h).view(batch_size*h*h, h, 1) + #yr = y.repeat(1, h, 1).view(batch_size*h*h, 1, h) + #dy = - yl.bmm(yr).view(batch_size, h, h, h, h) + + return dy + + +def batch_pinv_dx(x): + """ returns y = (x'*x)^-1 * x' and dy/dx. """ + # y = (x'*x)^-1 * x' + # = s^-1 * x' + # = b * x' + # d{y(j,k)}/d{x(m,n)} + # = d{b(j,i) * x(k,i)}/d{x(m,n)} + # = d{b(j,i)}/d{x(m,n)} * x(k,i) + b(j,i) * d{x(k,i)}/d{x(m,n)} + # d{b(j,i)}/d{x(m,n)} + # = d{b(j,i)}/d{s(p,q)} * d{s(p,q)}/d{x(m,n)} + # = -b(j,p)*b(q,i) * d{s(p,q)}/d{x(m,n)} + # d{s(p,q)}/d{x(m,n)} + # = d{x(t,p)*x(t,q)}/d{x(m,n)} + # = d{x(t,p)}/d{x(m,n)} * x(t,q) + x(t,p) * d{x(t,q)}/d{x(m,n)} + batch_size, h, w = x.size() + xt = x.transpose(1, 2) + s = xt.bmm(x) + b = batch_inverse(s) + y = b.bmm(xt) + + # dx/dx + ex = torch.eye(h*w).to(x).unsqueeze(0).view(1, h, w, h, w) + # ds/dx = dx(t,_)/dx * x(t,_) + x(t,_) * dx(t,_)/dx + ex1 = ex.view(1, h, w*h*w) # [t, p*m*n] + dx1 = x.transpose(1, 2).matmul(ex1).view(batch_size, w, w, h, w) # [q, p,m,n] + ds_dx = dx1.transpose(1, 2) + dx1 # [p, q, m, n] + # db/ds + db_ds = batch_inverse_dx(b) # [j, i, p, q] + # db/dx = db/d{s(p,q)} * d{s(p,q)}/dx + db1 = db_ds.view(batch_size, w*w, w*w).bmm(ds_dx.view(batch_size, w*w, h*w)) + db_dx = db1.view(batch_size, w, w, h, w) # [j, i, m, n] + # dy/dx = db(_,i)/dx * x(_,i) + b(_,i) * dx(_,i)/dx + dy1 = db_dx.transpose(1, 2).contiguous().view(batch_size, w, w*h*w) + dy1 = x.matmul(dy1).view(batch_size, h, w, h, w) # [k, j, m, n] + ext = ex.transpose(1, 2).contiguous().view(1, w, h*h*w) + dy2 = b.matmul(ext).view(batch_size, w, h, h, w) # [j, k, m, n] + dy_dx = dy1.transpose(1, 2) + dy2 + + return y, dy_dx + + +class InvMatrix(torch.autograd.Function): + """ M(n) -> M(n); x -> x^-1. + """ + @staticmethod + def forward(ctx, x): + y = batch_inverse(x) + ctx.save_for_backward(y) + return y + + @staticmethod + def backward(ctx, grad_output): + y, = ctx.saved_tensors # v0.4 + #y, = ctx.saved_variables # v0.3.1 + batch_size, h, w = y.size() + assert h == w + + # Let y(x) = x^-1 and assume any function f(y(x)). + # compute df/dx(m,n)... + # df/dx(m,n) = df/dy(j,k) * dy(j,k)/dx(m,n) + # well, df/dy is 'grad_output' + # and so we will return 'grad_input = df/dy(j,k) * dy(j,k)/dx(m,n)' + + dy = batch_inverse_dx(y) # dy(j,k,m,n) = dy(j,k)/dx(m,n) + go = grad_output.contiguous().view(batch_size, 1, h*h) # [1, (j*k)] + ym = dy.view(batch_size, h*h, h*h) # [(j*k), (m*n)] + r = go.bmm(ym) # [1, (m*n)] + grad_input = r.view(batch_size, h, h) # [m, n] + + return grad_input + + + +if __name__ == '__main__': + def test(): + x = torch.randn(2, 3, 2) + x_val = x.requires_grad_() + + s_val = x_val.transpose(1, 2).bmm(x_val) + s_inv = InvMatrix.apply(s_val) + y_val = s_inv.bmm(x_val.transpose(1, 2)) + y_val.sum().backward() + t1 = x_val.grad + + y, dy_dx = batch_pinv_dx(x) + t2 = dy_dx.sum(1).sum(1) + + print(t1) + print(t2) + print(t1 - t2) + + test() + +#EOF diff --git a/thirdparty/learning3d/ops/quaternion.py b/thirdparty/learning3d/ops/quaternion.py new file mode 100644 index 0000000000000000000000000000000000000000..fec4e61e3cae3f98ac390a95fec88214999ddea1 --- /dev/null +++ b/thirdparty/learning3d/ops/quaternion.py @@ -0,0 +1,218 @@ +# Copyright (c) 2018-present, Facebook, Inc. +# All rights reserved. +# +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. +# + +import torch +import numpy as np + +# PyTorch-backed implementations + + +def qmul(q, r): + """ + Multiply quaternion(s) q with quaternion(s) r. + Expects two equally-sized tensors of shape (*, 4), where * denotes any number of dimensions. + Returns q*r as a tensor of shape (*, 4). + """ + assert q.shape[-1] == 4 + assert r.shape[-1] == 4 + + original_shape = q.shape + + # Compute outer product + terms = torch.bmm(r.view(-1, 4, 1), q.view(-1, 1, 4)) + + w = terms[:, 0, 0] - terms[:, 1, 1] - terms[:, 2, 2] - terms[:, 3, 3] + x = terms[:, 0, 1] + terms[:, 1, 0] - terms[:, 2, 3] + terms[:, 3, 2] + y = terms[:, 0, 2] + terms[:, 1, 3] + terms[:, 2, 0] - terms[:, 3, 1] + z = terms[:, 0, 3] - terms[:, 1, 2] + terms[:, 2, 1] + terms[:, 3, 0] + return torch.stack((w, x, y, z), dim=1).view(original_shape) + + +def qrot(q, v): + """ + Rotate vector(s) v about the rotation described by quaternion(s) q. + Expects a tensor of shape (*, 4) for q and a tensor of shape (*, 3) for v, + where * denotes any number of dimensions. + Returns a tensor of shape (*, 3). + """ + assert q.shape[-1] == 4 + assert v.shape[-1] == 3 + assert q.shape[:-1] == v.shape[:-1] + + original_shape = list(v.shape) + q = q.view(-1, 4) + v = v.view(-1, 3) + + qvec = q[:, 1:] + uv = torch.cross(qvec, v, dim=1) + uuv = torch.cross(qvec, uv, dim=1) + return (v + 2 * (q[:, :1] * uv + uuv)).view(original_shape) + + +def qeuler(q, order, epsilon=0): + """ + Convert quaternion(s) q to Euler angles. + Expects a tensor of shape (*, 4), where * denotes any number of dimensions. + Returns a tensor of shape (*, 3). + """ + assert q.shape[-1] == 4 + + original_shape = list(q.shape) + original_shape[-1] = 3 + q = q.view(-1, 4) + + q0 = q[:, 0] + q1 = q[:, 1] + q2 = q[:, 2] + q3 = q[:, 3] + + if order == "xyz": + x = torch.atan2(2 * (q0 * q1 - q2 * q3), 1 - 2 * (q1 * q1 + q2 * q2)) + y = torch.asin(torch.clamp(2 * (q1 * q3 + q0 * q2), -1 + epsilon, 1 - epsilon)) + z = torch.atan2(2 * (q0 * q3 - q1 * q2), 1 - 2 * (q2 * q2 + q3 * q3)) + elif order == "yzx": + x = torch.atan2(2 * (q0 * q1 - q2 * q3), 1 - 2 * (q1 * q1 + q3 * q3)) + y = torch.atan2(2 * (q0 * q2 - q1 * q3), 1 - 2 * (q2 * q2 + q3 * q3)) + z = torch.asin(torch.clamp(2 * (q1 * q2 + q0 * q3), -1 + epsilon, 1 - epsilon)) + elif order == "zxy": + x = torch.asin(torch.clamp(2 * (q0 * q1 + q2 * q3), -1 + epsilon, 1 - epsilon)) + y = torch.atan2(2 * (q0 * q2 - q1 * q3), 1 - 2 * (q1 * q1 + q2 * q2)) + z = torch.atan2(2 * (q0 * q3 - q1 * q2), 1 - 2 * (q1 * q1 + q3 * q3)) + elif order == "xzy": + x = torch.atan2(2 * (q0 * q1 + q2 * q3), 1 - 2 * (q1 * q1 + q3 * q3)) + y = torch.atan2(2 * (q0 * q2 + q1 * q3), 1 - 2 * (q2 * q2 + q3 * q3)) + z = torch.asin(torch.clamp(2 * (q0 * q3 - q1 * q2), -1 + epsilon, 1 - epsilon)) + elif order == "yxz": + x = torch.asin(torch.clamp(2 * (q0 * q1 - q2 * q3), -1 + epsilon, 1 - epsilon)) + y = torch.atan2(2 * (q1 * q3 + q0 * q2), 1 - 2 * (q1 * q1 + q2 * q2)) + z = torch.atan2(2 * (q1 * q2 + q0 * q3), 1 - 2 * (q1 * q1 + q3 * q3)) + elif order == "zyx": + x = torch.atan2(2 * (q0 * q1 + q2 * q3), 1 - 2 * (q1 * q1 + q2 * q2)) + y = torch.asin(torch.clamp(2 * (q0 * q2 - q1 * q3), -1 + epsilon, 1 - epsilon)) + z = torch.atan2(2 * (q0 * q3 + q1 * q2), 1 - 2 * (q2 * q2 + q3 * q3)) + else: + raise + + return torch.stack((x, y, z), dim=1).view(original_shape) + + +# Numpy-backed implementations + + +def qmul_np(q, r): + q = torch.from_numpy(q).contiguous() + r = torch.from_numpy(r).contiguous() + return qmul(q, r).numpy() + + +def qrot_np(q, v): + q = torch.from_numpy(q).contiguous() + v = torch.from_numpy(v).contiguous() + return qrot(q, v).numpy() + + +def qeuler_np(q, order, epsilon=0, use_gpu=False): + if use_gpu: + q = torch.from_numpy(q).cuda() + return qeuler(q, order, epsilon).cpu().numpy() + else: + q = torch.from_numpy(q).contiguous() + return qeuler(q, order, epsilon).numpy() + + +def qfix(q): + """ + Enforce quaternion continuity across the time dimension by selecting + the representation (q or -q) with minimal distance (or, equivalently, maximal dot product) + between two consecutive frames. + + Expects a tensor of shape (L, J, 4), where L is the sequence length and J is the number of joints. + Returns a tensor of the same shape. + """ + assert len(q.shape) == 3 + assert q.shape[-1] == 4 + + result = q.copy() + dot_products = np.sum(q[1:] * q[:-1], axis=2) + mask = dot_products < 0 + mask = (np.cumsum(mask, axis=0) % 2).astype(bool) + result[1:][mask] *= -1 + return result + + +def expmap_to_quaternion(e): + """ + Convert axis-angle rotations (aka exponential maps) to quaternions. + Stable formula from "Practical Parameterization of Rotations Using the Exponential Map". + Expects a tensor of shape (*, 3), where * denotes any number of dimensions. + Returns a tensor of shape (*, 4). + """ + assert e.shape[-1] == 3 + + original_shape = list(e.shape) + original_shape[-1] = 4 + e = e.reshape(-1, 3) + + theta = np.linalg.norm(e, axis=1).reshape(-1, 1) + w = np.cos(0.5 * theta).reshape(-1, 1) + xyz = 0.5 * np.sinc(0.5 * theta / np.pi) * e + return np.concatenate((w, xyz), axis=1).reshape(original_shape) + + +def euler_to_quaternion(e, order): + """ + Convert Euler angles to quaternions. + """ + assert e.shape[-1] == 3 + + original_shape = list(e.shape) + original_shape[-1] = 4 + + e = e.reshape(-1, 3) + + x = e[:, 0] + y = e[:, 1] + z = e[:, 2] + + rx = np.stack( + (np.cos(x / 2), np.sin(x / 2), np.zeros_like(x), np.zeros_like(x)), axis=1 + ) + ry = np.stack( + (np.cos(y / 2), np.zeros_like(y), np.sin(y / 2), np.zeros_like(y)), axis=1 + ) + rz = np.stack( + (np.cos(z / 2), np.zeros_like(z), np.zeros_like(z), np.sin(z / 2)), axis=1 + ) + + result = None + for coord in order: + if coord == "x": + r = rx + elif coord == "y": + r = ry + elif coord == "z": + r = rz + else: + raise + if result is None: + result = r + else: + result = qmul_np(result, r) + + # Reverse antipodal representation to have a non-negative "w" + if order in ["xyz", "yzx", "zxy"]: + result *= -1 + + return result.reshape(original_shape) + + +def qinv(q): + # expectes q in (w,x,y,z) format + w = q[:, 0:1] + v = q[:, 1:] + inv = torch.cat([w, -v], dim=1) + return inv diff --git a/thirdparty/learning3d/ops/se3.py b/thirdparty/learning3d/ops/se3.py new file mode 100644 index 0000000000000000000000000000000000000000..5834516d8ddf1c82c87b3223edc88c4eb0b83631 --- /dev/null +++ b/thirdparty/learning3d/ops/se3.py @@ -0,0 +1,157 @@ +""" 3-d rigid body transfomation group and corresponding Lie algebra. """ +import torch +from .sinc import sinc1, sinc2, sinc3 +from . import so3 + +def twist_prod(x, y): + x_ = x.view(-1, 6) + y_ = y.view(-1, 6) + + xw, xv = x_[:, 0:3], x_[:, 3:6] + yw, yv = y_[:, 0:3], y_[:, 3:6] + + zw = so3.cross_prod(xw, yw) + zv = so3.cross_prod(xw, yv) + so3.cross_prod(xv, yw) + + z = torch.cat((zw, zv), dim=1) + + return z.view_as(x) + +def liebracket(x, y): + return twist_prod(x, y) + + +def mat(x): + # size: [*, 6] -> [*, 4, 4] + x_ = x.view(-1, 6) + w1, w2, w3 = x_[:, 0], x_[:, 1], x_[:, 2] + v1, v2, v3 = x_[:, 3], x_[:, 4], x_[:, 5] + O = torch.zeros_like(w1) + + X = torch.stack(( + torch.stack(( O, -w3, w2, v1), dim=1), + torch.stack(( w3, O, -w1, v2), dim=1), + torch.stack((-w2, w1, O, v3), dim=1), + torch.stack(( O, O, O, O), dim=1)), dim=1) + return X.view(*(x.size()[0:-1]), 4, 4) + +def vec(X): + X_ = X.view(-1, 4, 4) + w1, w2, w3 = X_[:, 2, 1], X_[:, 0, 2], X_[:, 1, 0] + v1, v2, v3 = X_[:, 0, 3], X_[:, 1, 3], X_[:, 2, 3] + x = torch.stack((w1, w2, w3, v1, v2, v3), dim=1) + return x.view(*X.size()[0:-2], 6) + +def genvec(): + return torch.eye(6) + +def genmat(): + return mat(genvec()) + +def exp(x): + x_ = x.view(-1, 6) + w, v = x_[:, 0:3], x_[:, 3:6] + t = w.norm(p=2, dim=1).view(-1, 1, 1) + W = so3.mat(w) + S = W.bmm(W) + I = torch.eye(3).to(w) + + # Rodrigues' rotation formula. + #R = cos(t)*eye(3) + sinc1(t)*W + sinc2(t)*(w*w'); + # = eye(3) + sinc1(t)*W + sinc2(t)*S + R = I + sinc1(t)*W + sinc2(t)*S + + #V = sinc1(t)*eye(3) + sinc2(t)*W + sinc3(t)*(w*w') + # = eye(3) + sinc2(t)*W + sinc3(t)*S + V = I + sinc2(t)*W + sinc3(t)*S + + p = V.bmm(v.contiguous().view(-1, 3, 1)) + + z = torch.Tensor([0, 0, 0, 1]).view(1, 1, 4).repeat(x_.size(0), 1, 1).to(x) + Rp = torch.cat((R, p), dim=2) + g = torch.cat((Rp, z), dim=1) + + return g.view(*(x.size()[0:-1]), 4, 4) + +def inverse(g): + g_ = g.view(-1, 4, 4) + R = g_[:, 0:3, 0:3] + p = g_[:, 0:3, 3] + Q = R.transpose(1, 2) + q = -Q.matmul(p.unsqueeze(-1)) + + z = torch.Tensor([0, 0, 0, 1]).view(1, 1, 4).repeat(g_.size(0), 1, 1).to(g) + Qq = torch.cat((Q, q), dim=2) + ig = torch.cat((Qq, z), dim=1) + + return ig.view(*(g.size()[0:-2]), 4, 4) + + +def log(g): + g_ = g.view(-1, 4, 4) + R = g_[:, 0:3, 0:3] + p = g_[:, 0:3, 3] + + w = so3.log(R) + H = so3.inv_vecs_Xg_ig(w) + v = H.bmm(p.contiguous().view(-1, 3, 1)).view(-1, 3) + + x = torch.cat((w, v), dim=1) + return x.view(*(g.size()[0:-2]), 6) + +def transform(g, a): + # g : SE(3), * x 4 x 4 + # a : R^3, * x 3[x N] + g_ = g.view(-1, 4, 4) + R = g_[:, 0:3, 0:3].contiguous().view(*(g.size()[0:-2]), 3, 3) + p = g_[:, 0:3, 3].contiguous().view(*(g.size()[0:-2]), 3) + if len(g.size()) == len(a.size()): + b = R.matmul(a) + p.unsqueeze(-1) + else: + b = R.matmul(a.unsqueeze(-1)).squeeze(-1) + p + return b + +def group_prod(g, h): + # g, h : SE(3) + g1 = g.matmul(h) + return g1 + + +class ExpMap(torch.autograd.Function): + """ Exp: se(3) -> SE(3) + """ + @staticmethod + def forward(ctx, x): + """ Exp: R^6 -> M(4), + size: [B, 6] -> [B, 4, 4], + or [B, 1, 6] -> [B, 1, 4, 4] + """ + ctx.save_for_backward(x) + g = exp(x) + return g + + @staticmethod + def backward(ctx, grad_output): + x, = ctx.saved_tensors + g = exp(x) + gen_k = genmat().to(x) + + # Let z = f(g) = f(exp(x)) + # dz = df/dgij * dgij/dxk * dxk + # = df/dgij * (d/dxk)[exp(x)]_ij * dxk + # = df/dgij * [gen_k*g]_ij * dxk + + dg = gen_k.matmul(g.view(-1, 1, 4, 4)) + # (k, i, j) + dg = dg.to(grad_output) + + go = grad_output.contiguous().view(-1, 1, 4, 4) + dd = go * dg + grad_input = dd.sum(-1).sum(-1) + + return grad_input + +Exp = ExpMap.apply + + +#EOF diff --git a/thirdparty/learning3d/ops/sinc.py b/thirdparty/learning3d/ops/sinc.py new file mode 100644 index 0000000000000000000000000000000000000000..67e33637b78c20719ae4e9f00a82fa2393d09841 --- /dev/null +++ b/thirdparty/learning3d/ops/sinc.py @@ -0,0 +1,229 @@ +""" sinc(t) := sin(t) / t """ +import torch +from torch import sin, cos + +def sinc1(t): + """ sinc1: t -> sin(t)/t """ + e = 0.01 + r = torch.zeros_like(t) + a = torch.abs(t) + + s = a < e + c = (s == 0) + t2 = t[s] ** 2 + r[s] = 1 - t2/6*(1 - t2/20*(1 - t2/42)) # Taylor series O(t^8) + r[c] = sin(t[c]) / t[c] + + return r + +def sinc1_dt(t): + """ d/dt(sinc1) """ + e = 0.01 + r = torch.zeros_like(t) + a = torch.abs(t) + + s = a < e + c = (s == 0) + t2 = t ** 2 + r[s] = -t[s]/3*(1 - t2[s]/10*(1 - t2[s]/28*(1 - t2[s]/54))) # Taylor series O(t^8) + r[c] = cos(t[c])/t[c] - sin(t[c])/t2[c] + + return r + +def sinc1_dt_rt(t): + """ d/dt(sinc1) / t """ + e = 0.01 + r = torch.zeros_like(t) + a = torch.abs(t) + + s = a < e + c = (s == 0) + t2 = t ** 2 + r[s] = -1/3*(1 - t2[s]/10*(1 - t2[s]/28*(1 - t2[s]/54))) # Taylor series O(t^8) + r[c] = (cos(t[c]) / t[c] - sin(t[c]) / t2[c]) / t[c] + + return r + + +def rsinc1(t): + """ rsinc1: t -> t/sinc1(t) """ + e = 0.01 + r = torch.zeros_like(t) + a = torch.abs(t) + + s = a < e + c = (s == 0) + t2 = t[s] ** 2 + r[s] = (((31*t2)/42 + 7)*t2/60 + 1)*t2/6 + 1 # Taylor series O(t^8) + r[c] = t[c] / sin(t[c]) + + return r + +def rsinc1_dt(t): + """ d/dt(rsinc1) """ + e = 0.01 + r = torch.zeros_like(t) + a = torch.abs(t) + + s = a < e + c = (s == 0) + t2 = t[s] ** 2 + r[s] = ((((127*t2)/30 + 31)*t2/28 + 7)*t2/30 + 1)*t[s]/3 # Taylor series O(t^8) + r[c] = 1/sin(t[c]) - (t[c]*cos(t[c]))/(sin(t[c])*sin(t[c])) + + return r + +def rsinc1_dt_csc(t): + """ d/dt(rsinc1) / sin(t) """ + e = 0.01 + r = torch.zeros_like(t) + a = torch.abs(t) + + s = a < e + c = (s == 0) + t2 = t[s] ** 2 + r[s] = t2*(t2*((4*t2)/675 + 2/63) + 2/15) + 1/3 # Taylor series O(t^8) + r[c] = (1/sin(t[c]) - (t[c]*cos(t[c]))/(sin(t[c])*sin(t[c]))) / sin(t[c]) + + return r + + +def sinc2(t): + """ sinc2: t -> (1 - cos(t)) / (t**2) """ + e = 0.01 + r = torch.zeros_like(t) + a = torch.abs(t) + + s = a < e + c = (s == 0) + t2 = t ** 2 + r[s] = 1/2*(1-t2[s]/12*(1-t2[s]/30*(1-t2[s]/56))) # Taylor series O(t^8) + r[c] = (1-cos(t[c]))/t2[c] + + return r + +def sinc2_dt(t): + """ d/dt(sinc2) """ + e = 0.01 + r = torch.zeros_like(t) + a = torch.abs(t) + + s = a < e + c = (s == 0) + t2 = t ** 2 + r[s] = -t[s]/12*(1 - t2[s]/5*(1.0/3 - t2[s]/56*(1.0/2 - t2[s]/135))) # Taylor series O(t^8) + r[c] = sin(t[c])/t2[c] - 2*(1-cos(t[c]))/(t2[c]*t[c]) + + return r + + +def sinc3(t): + """ sinc3: t -> (t - sin(t)) / (t**3) """ + e = 0.01 + r = torch.zeros_like(t) + a = torch.abs(t) + + s = a < e + c = (s == 0) + t2 = t[s] ** 2 + r[s] = 1/6*(1-t2/20*(1-t2/42*(1-t2/72))) # Taylor series O(t^8) + r[c] = (t[c]-sin(t[c]))/(t[c]**3) + + return r + +def sinc3_dt(t): + """ d/dt(sinc3) """ + e = 0.01 + r = torch.zeros_like(t) + a = torch.abs(t) + + s = a < e + c = (s == 0) + t2 = t[s] ** 2 + r[s] = -t[s]/60*(1 - t2/21*(1 - t2/24*(1.0/2 - t2/165))) # Taylor series O(t^8) + r[c] = (3*sin(t[c]) - t[c]*(cos(t[c]) + 2))/(t[c]**4) + + return r + + +def sinc4(t): + """ sinc4: t -> 1/t^2 * (1/2 - sinc2(t)) + = 1/t^2 * (1/2 - (1 - cos(t))/t^2) + """ + e = 0.01 + r = torch.zeros_like(t) + a = torch.abs(t) + + s = a < e + c = (s == 0) + t2 = t ** 2 + r[s] = 1/24*(1-t2/30*(1-t2/56*(1-t2/90))) # Taylor series O(t^8) + r[c] = (0.5 - (1 - cos(t))/t2) / t2 + + +class Sinc1_autograd(torch.autograd.Function): + @staticmethod + def forward(ctx, theta): + ctx.save_for_backward(theta) + return sinc1(theta) + + @staticmethod + def backward(ctx, grad_output): + theta, = ctx.saved_tensors + grad_theta = None + if ctx.needs_input_grad[0]: + grad_theta = grad_output * sinc1_dt(theta).to(grad_output) + return grad_theta + +Sinc1 = Sinc1_autograd.apply + +class RSinc1_autograd(torch.autograd.Function): + @staticmethod + def forward(ctx, theta): + ctx.save_for_backward(theta) + return rsinc1(theta) + + @staticmethod + def backward(ctx, grad_output): + theta, = ctx.saved_tensors + grad_theta = None + if ctx.needs_input_grad[0]: + grad_theta = grad_output * rsinc1_dt(theta).to(grad_output) + return grad_theta + +RSinc1 = RSinc1_autograd.apply + +class Sinc2_autograd(torch.autograd.Function): + @staticmethod + def forward(ctx, theta): + ctx.save_for_backward(theta) + return sinc2(theta) + + @staticmethod + def backward(ctx, grad_output): + theta, = ctx.saved_tensors + grad_theta = None + if ctx.needs_input_grad[0]: + grad_theta = grad_output * sinc2_dt(theta).to(grad_output) + return grad_theta + +Sinc2 = Sinc2_autograd.apply + +class Sinc3_autograd(torch.autograd.Function): + @staticmethod + def forward(ctx, theta): + ctx.save_for_backward(theta) + return sinc3(theta) + + @staticmethod + def backward(ctx, grad_output): + theta, = ctx.saved_tensors + grad_theta = None + if ctx.needs_input_grad[0]: + grad_theta = grad_output * sinc3_dt(theta).to(grad_output) + return grad_theta + +Sinc3 = Sinc3_autograd.apply + + +#EOF diff --git a/thirdparty/learning3d/ops/so3.py b/thirdparty/learning3d/ops/so3.py new file mode 100644 index 0000000000000000000000000000000000000000..fc165d7c69f60fccfaee0384d15d198b17cb4523 --- /dev/null +++ b/thirdparty/learning3d/ops/so3.py @@ -0,0 +1,213 @@ +""" 3-d rotation group and corresponding Lie algebra """ +import torch +from . import sinc +from .sinc import sinc1, sinc2, sinc3 + + +def cross_prod(x, y): + z = torch.cross(x.view(-1, 3), y.view(-1, 3), dim=1).view_as(x) + return z + +def liebracket(x, y): + return cross_prod(x, y) + +def mat(x): + # size: [*, 3] -> [*, 3, 3] + x_ = x.view(-1, 3) + x1, x2, x3 = x_[:, 0], x_[:, 1], x_[:, 2] + O = torch.zeros_like(x1) + + X = torch.stack(( + torch.stack((O, -x3, x2), dim=1), + torch.stack((x3, O, -x1), dim=1), + torch.stack((-x2, x1, O), dim=1)), dim=1) + return X.view(*(x.size()[0:-1]), 3, 3) + +def vec(X): + X_ = X.view(-1, 3, 3) + x1, x2, x3 = X_[:, 2, 1], X_[:, 0, 2], X_[:, 1, 0] + x = torch.stack((x1, x2, x3), dim=1) + return x.view(*X.size()[0:-2], 3) + +def genvec(): + return torch.eye(3) + +def genmat(): + return mat(genvec()) + +def RodriguesRotation(x): + # for autograd + w = x.view(-1, 3) + t = w.norm(p=2, dim=1).view(-1, 1, 1) + W = mat(w) + S = W.bmm(W) + I = torch.eye(3).to(w) + + # Rodrigues' rotation formula. + #R = cos(t)*eye(3) + sinc1(t)*W + sinc2(t)*(w*w'); + #R = eye(3) + sinc1(t)*W + sinc2(t)*S + + R = I + sinc.Sinc1(t)*W + sinc.Sinc2(t)*S + + return R.view(*(x.size()[0:-1]), 3, 3) + +def exp(x): + w = x.view(-1, 3) + t = w.norm(p=2, dim=1).view(-1, 1, 1) + W = mat(w) + S = W.bmm(W) + I = torch.eye(3).to(w) + + # Rodrigues' rotation formula. + #R = cos(t)*eye(3) + sinc1(t)*W + sinc2(t)*(w*w'); + #R = eye(3) + sinc1(t)*W + sinc2(t)*S + + R = I + sinc1(t)*W + sinc2(t)*S + + return R.view(*(x.size()[0:-1]), 3, 3) + +def inverse(g): + R = g.view(-1, 3, 3) + Rt = R.transpose(1, 2) + return Rt.view_as(g) + +def btrace(X): + # batch-trace: [B, N, N] -> [B] + n = X.size(-1) + X_ = X.view(-1, n, n) + tr = torch.zeros(X_.size(0)).to(X) + for i in range(tr.size(0)): + m = X_[i, :, :] + tr[i] = torch.trace(m) + return tr.view(*(X.size()[0:-2])) + +def log(g): + eps = 1.0e-7 + R = g.view(-1, 3, 3) + tr = btrace(R) + c = (tr - 1) / 2 + t = torch.acos(c) + sc = sinc1(t) + idx0 = (torch.abs(sc) <= eps) + idx1 = (torch.abs(sc) > eps) + sc = sc.view(-1, 1, 1) + + X = torch.zeros_like(R) + if idx1.any(): + X[idx1] = (R[idx1] - R[idx1].transpose(1, 2)) / (2*sc[idx1]) + + if idx0.any(): + # t[idx0] == math.pi + t2 = t[idx0] ** 2 + A = (R[idx0] + torch.eye(3).type_as(R).unsqueeze(0)) * t2.view(-1, 1, 1) / 2 + aw1 = torch.sqrt(A[:, 0, 0]) + aw2 = torch.sqrt(A[:, 1, 1]) + aw3 = torch.sqrt(A[:, 2, 2]) + sgn_3 = torch.sign(A[:, 0, 2]) + sgn_3[sgn_3 == 0] = 1 + sgn_23 = torch.sign(A[:, 1, 2]) + sgn_23[sgn_23 == 0] = 1 + sgn_2 = sgn_23 * sgn_3 + w1 = aw1 + w2 = aw2 * sgn_2 + w3 = aw3 * sgn_3 + w = torch.stack((w1, w2, w3), dim=-1) + W = mat(w) + X[idx0] = W + + x = vec(X.view_as(g)) + return x + +def transform(g, a): + # g in SO(3): * x 3 x 3 + # a in R^3: * x 3[x N] + if len(g.size()) == len(a.size()): + b = g.matmul(a) + else: + b = g.matmul(a.unsqueeze(-1)).squeeze(-1) + return b + +def group_prod(g, h): + # g, h : SO(3) + g1 = g.matmul(h) + return g1 + + + +def vecs_Xg_ig(x): + """ Vi = vec(dg/dxi * inv(g)), where g = exp(x) + (== [Ad(exp(x))] * vecs_ig_Xg(x)) + """ + t = x.view(-1, 3).norm(p=2, dim=1).view(-1, 1, 1) + X = mat(x) + S = X.bmm(X) + #B = x.view(-1,3,1).bmm(x.view(-1,1,3)) # B = x*x' + I = torch.eye(3).to(X) + + #V = sinc1(t)*eye(3) + sinc2(t)*X + sinc3(t)*B + #V = eye(3) + sinc2(t)*X + sinc3(t)*S + + V = I + sinc2(t)*X + sinc3(t)*S + + return V.view(*(x.size()[0:-1]), 3, 3) + +def inv_vecs_Xg_ig(x): + """ H = inv(vecs_Xg_ig(x)) """ + t = x.view(-1, 3).norm(p=2, dim=1).view(-1, 1, 1) + X = mat(x) + S = X.bmm(X) + I = torch.eye(3).to(x) + + e = 0.01 + eta = torch.zeros_like(t) + s = (t < e) + c = (s == 0) + t2 = t[s] ** 2 + eta[s] = ((t2/40 + 1)*t2/42 + 1)*t2/720 + 1/12 # O(t**8) + eta[c] = (1 - (t[c]/2) / torch.tan(t[c]/2)) / (t[c]**2) + + H = I - 1/2*X + eta*S + return H.view(*(x.size()[0:-1]), 3, 3) + + +class ExpMap(torch.autograd.Function): + """ Exp: so(3) -> SO(3) + """ + @staticmethod + def forward(ctx, x): + """ Exp: R^3 -> M(3), + size: [B, 3] -> [B, 3, 3], + or [B, 1, 3] -> [B, 1, 3, 3] + """ + ctx.save_for_backward(x) + g = exp(x) + return g + + @staticmethod + def backward(ctx, grad_output): + x, = ctx.saved_tensors + g = exp(x) + gen_k = genmat().to(x) + #gen_1 = gen_k[0, :, :] + #gen_2 = gen_k[1, :, :] + #gen_3 = gen_k[2, :, :] + + # Let z = f(g) = f(exp(x)) + # dz = df/dgij * dgij/dxk * dxk + # = df/dgij * (d/dxk)[exp(x)]_ij * dxk + # = df/dgij * [gen_k*g]_ij * dxk + + dg = gen_k.matmul(g.view(-1, 1, 3, 3)) + # (k, i, j) + dg = dg.to(grad_output) + + go = grad_output.contiguous().view(-1, 1, 3, 3) + dd = go * dg + grad_input = dd.sum(-1).sum(-1) + + return grad_input + +Exp = ExpMap.apply + + +#EOF diff --git a/thirdparty/learning3d/ops/transform_functions.py b/thirdparty/learning3d/ops/transform_functions.py new file mode 100644 index 0000000000000000000000000000000000000000..fc6de1f6b8f35c08002b1381de16d31ab4e5a75e --- /dev/null +++ b/thirdparty/learning3d/ops/transform_functions.py @@ -0,0 +1,342 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F +import numpy as np +from . import quaternion # works with (w, x, y, z) quaternions +from scipy.spatial.transform import Rotation +from . import se3 + + +def quat2mat(quat): + x, y, z, w = quat[:, 0], quat[:, 1], quat[:, 2], quat[:, 3] + + B = quat.size(0) + + w2, x2, y2, z2 = w.pow(2), x.pow(2), y.pow(2), z.pow(2) + wx, wy, wz = w*x, w*y, w*z + xy, xz, yz = x*y, x*z, y*z + + rotMat = torch.stack([w2 + x2 - y2 - z2, 2*xy - 2*wz, 2*wy + 2*xz, + 2*wz + 2*xy, w2 - x2 + y2 - z2, 2*yz - 2*wx, + 2*xz - 2*wy, 2*wx + 2*yz, w2 - x2 - y2 + z2], dim=1).reshape(B, 3, 3) + return rotMat + +def transform_point_cloud(point_cloud: torch.Tensor, rotation: torch.Tensor, translation: torch.Tensor): + if len(rotation.size()) == 2: + rot_mat = quat2mat(rotation) + else: + rot_mat = rotation + return (torch.matmul(rot_mat, point_cloud.permute(0, 2, 1)) + translation.unsqueeze(2)).permute(0, 2, 1) + +def convert2transformation(rotation_matrix: torch.Tensor, translation_vector: torch.Tensor): + one_ = torch.tensor([[[0.0, 0.0, 0.0, 1.0]]]).repeat(rotation_matrix.shape[0], 1, 1).to(rotation_matrix) # (Bx1x4) + transformation_matrix = torch.cat([rotation_matrix, translation_vector.unsqueeze(-1)], dim=2) # (Bx3x4) + transformation_matrix = torch.cat([transformation_matrix, one_], dim=1) # (Bx4x4) + return transformation_matrix + +def qmul(q, r): + """ + Multiply quaternion(s) q with quaternion(s) r. + Expects two equally-sized tensors of shape (*, 4), where * denotes any number of dimensions. + Returns q*r as a tensor of shape (*, 4). + """ + assert q.shape[-1] == 4 + assert r.shape[-1] == 4 + + original_shape = q.shape + + # Compute outer product + terms = torch.bmm(r.view(-1, 4, 1), q.view(-1, 1, 4)) + + w = terms[:, 0, 0] - terms[:, 1, 1] - terms[:, 2, 2] - terms[:, 3, 3] + x = terms[:, 0, 1] + terms[:, 1, 0] - terms[:, 2, 3] + terms[:, 3, 2] + y = terms[:, 0, 2] + terms[:, 1, 3] + terms[:, 2, 0] - terms[:, 3, 1] + z = terms[:, 0, 3] - terms[:, 1, 2] + terms[:, 2, 1] + terms[:, 3, 0] + return torch.stack((w, x, y, z), dim=1).view(original_shape) + +def qmul_np(q, r): + q = torch.from_numpy(q).contiguous() + r = torch.from_numpy(r).contiguous() + return qmul(q, r).numpy() + +def euler_to_quaternion(e, order): + """ + Convert Euler angles to quaternions. + """ + assert e.shape[-1] == 3 + + original_shape = list(e.shape) + original_shape[-1] = 4 + + e = e.reshape(-1, 3) + + x = e[:, 0] + y = e[:, 1] + z = e[:, 2] + + rx = np.stack( + (np.cos(x / 2), np.sin(x / 2), np.zeros_like(x), np.zeros_like(x)), axis=1 + ) + ry = np.stack( + (np.cos(y / 2), np.zeros_like(y), np.sin(y / 2), np.zeros_like(y)), axis=1 + ) + rz = np.stack( + (np.cos(z / 2), np.zeros_like(z), np.zeros_like(z), np.sin(z / 2)), axis=1 + ) + + result = None + for coord in order: + if coord == "x": + r = rx + elif coord == "y": + r = ry + elif coord == "z": + r = rz + else: + raise + if result is None: + result = r + else: + result = qmul_np(result, r) + + # Reverse antipodal representation to have a non-negative "w" + if order in ["xyz", "yzx", "zxy"]: + result *= -1 + + return result.reshape(original_shape) + + +class PNLKTransform: + """ rigid motion """ + def __init__(self, mag=1, mag_randomly=False): + self.mag = mag + self.randomly = mag_randomly + + self.gt = None + self.igt = None + self.index = 0 + + def generate_transform(self): + # return: a twist-vector + amp = self.mag + if self.randomly: + amp = torch.rand(1, 1) * self.mag + x = torch.randn(1, 6) + x = x / x.norm(p=2, dim=1, keepdim=True) * amp + + return x # [1, 6] + + def apply_transform(self, p0, x): + # p0: [N, 3] + # x: [1, 6] + g = se3.exp(x).to(p0) # [1, 4, 4] + gt = se3.exp(-x).to(p0) # [1, 4, 4] + + p1 = se3.transform(g, p0) + self.gt = gt.squeeze(0) # gt: p1 -> p0 + self.igt = g.squeeze(0) # igt: p0 -> p1 + return p1 + + def transform(self, tensor): + x = self.generate_transform() + return self.apply_transform(tensor, x) + + def __call__(self, tensor): + return self.transform(tensor) + + +class RPMNetTransform: + """ rigid motion """ + def __init__(self, mag=1, mag_randomly=False): + self.mag = mag + self.randomly = mag_randomly + + self.gt = None + self.igt = None + self.index = 0 + + def generate_transform(self): + # return: a twist-vector + amp = self.mag + if self.randomly: + amp = torch.rand(1, 1) * self.mag + x = torch.randn(1, 6) + x = x / x.norm(p=2, dim=1, keepdim=True) * amp + + return x # [1, 6] + + def apply_transform(self, p0, x): + # p0: [N, 3] + # x: [1, 6] + g = se3.exp(x).to(p0) # [1, 4, 4] + gt = se3.exp(-x).to(p0) # [1, 4, 4] + + p1 = se3.transform(g, p0[:, :3]) + + if p0.shape[1] == 6: # Need to rotate normals also + g_n = g.clone() + g_n[:, :3, 3] = 0.0 + n1 = se3.transform(g_n, p0[:, 3:6]) + p1 = torch.cat([p1, n1], axis=-1) + + self.gt = gt.squeeze(0) # gt: p1 -> p0 + self.igt = g.squeeze(0) # igt: p0 -> p1 + return p1 + + def transform(self, tensor): + x = self.generate_transform() + return self.apply_transform(tensor, x) + + def __call__(self, tensor): + return self.transform(tensor) + + +class PCRNetTransform: + def __init__(self, data_size, angle_range=45, translation_range=1): + self.angle_range = angle_range + self.translation_range = translation_range + self.dtype = torch.float32 + self.transformations = [self.create_random_transform(torch.float32, self.angle_range, self.translation_range) for _ in range(data_size)] + self.index = 0 + + @staticmethod + def deg_to_rad(deg): + return np.pi / 180 * deg + + def create_random_transform(self, dtype, max_rotation_deg, max_translation): + max_rotation = self.deg_to_rad(max_rotation_deg) + rot = np.random.uniform(-max_rotation, max_rotation, [1, 3]) + trans = np.random.uniform(-max_translation, max_translation, [1, 3]) + quat = euler_to_quaternion(rot, "xyz") + + vec = np.concatenate([quat, trans], axis=1) + vec = torch.tensor(vec, dtype=dtype) + return vec + + @staticmethod + def create_pose_7d(vector: torch.Tensor): + # Normalize the quaternion. + pre_normalized_quaternion = vector[:, 0:4] + normalized_quaternion = F.normalize(pre_normalized_quaternion, dim=1) + + # B x 7 vector of 4 quaternions and 3 translation parameters + translation = vector[:, 4:] + vector = torch.cat([normalized_quaternion, translation], dim=1) + return vector.view([-1, 7]) + + @staticmethod + def get_quaternion(pose_7d: torch.Tensor): + return pose_7d[:, 0:4] + + @staticmethod + def get_translation(pose_7d: torch.Tensor): + return pose_7d[:, 4:] + + @staticmethod + def quaternion_rotate(point_cloud: torch.Tensor, pose_7d: torch.Tensor): + ndim = point_cloud.dim() + if ndim == 2: + N, _ = point_cloud.shape + assert pose_7d.shape[0] == 1 + # repeat transformation vector for each point in shape + quat = PCRNetTransform.get_quaternion(pose_7d).expand([N, -1]) + rotated_point_cloud = quaternion.qrot(quat, point_cloud) + + elif ndim == 3: + B, N, _ = point_cloud.shape + quat = PCRNetTransform.get_quaternion(pose_7d).unsqueeze(1).expand([-1, N, -1]).contiguous() + rotated_point_cloud = quaternion.qrot(quat, point_cloud) + + return rotated_point_cloud + + @staticmethod + def quaternion_transform(point_cloud: torch.Tensor, pose_7d: torch.Tensor): + transformed_point_cloud = PCRNetTransform.quaternion_rotate(point_cloud, pose_7d) + PCRNetTransform.get_translation(pose_7d).view(-1, 1, 3).repeat(1, point_cloud.shape[1], 1) # Ps' = R*Ps + t + return transformed_point_cloud + + @staticmethod + def convert2transformation(rotation_matrix: torch.Tensor, translation_vector: torch.Tensor): + one_ = torch.tensor([[[0.0, 0.0, 0.0, 1.0]]]).repeat(rotation_matrix.shape[0], 1, 1).to(rotation_matrix) # (Bx1x4) + transformation_matrix = torch.cat([rotation_matrix, translation_vector[:,0,:].unsqueeze(-1)], dim=2) # (Bx3x4) + transformation_matrix = torch.cat([transformation_matrix, one_], dim=1) # (Bx4x4) + return transformation_matrix + + def __call__(self, template): + self.igt = self.transformations[self.index] + gt = self.create_pose_7d(self.igt) + source = self.quaternion_rotate(template, gt) + self.get_translation(gt) + return source + + +class DCPTransform: + def __init__(self, angle_range=45, translation_range=1): + self.angle_range = angle_range*(np.pi/180) + self.translation_range = translation_range + self.index = 0 + + def generate_transform(self): + self.anglex = np.random.uniform() * self.angle_range + self.angley = np.random.uniform() * self.angle_range + self.anglez = np.random.uniform() * self.angle_range + self.translation = np.array([np.random.uniform(-self.translation_range, self.translation_range), + np.random.uniform(-self.translation_range, self.translation_range), + np.random.uniform(-self.translation_range, self.translation_range)]) + # cosx = np.cos(self.anglex) + # cosy = np.cos(self.angley) + # cosz = np.cos(self.anglez) + # sinx = np.sin(self.anglex) + # siny = np.sin(self.angley) + # sinz = np.sin(self.anglez) + # Rx = np.array([[1, 0, 0], + # [0, cosx, -sinx], + # [0, sinx, cosx]]) + # Ry = np.array([[cosy, 0, siny], + # [0, 1, 0], + # [-siny, 0, cosy]]) + # Rz = np.array([[cosz, -sinz, 0], + # [sinz, cosz, 0], + # [0, 0, 1]]) + # self.R_ab = Rx.dot(Ry).dot(Rz) + # last_row = np.array([[0., 0., 0., 1.]]) + # self.igt = np.concatenate([self.R_ab, self.translation_ab.reshape(-1,1)], axis=1) + # self.igt = np.concatenate([self.igt, last_row], axis=0) + + def apply_transformation(self, template): + rotation = Rotation.from_euler('zyx', [self.anglez, self.angley, self.anglex]) + self.igt = rotation.apply(np.eye(3)) + self.igt = np.concatenate([self.igt, self.translation.reshape(-1,1)], axis=1) + self.igt = torch.from_numpy(np.concatenate([self.igt, np.array([[0., 0., 0., 1.]])], axis=0)).float() + source = rotation.apply(template) + np.expand_dims(self.translation, axis=0) + return source + + def __call__(self, template): + template = template.numpy() + self.generate_transform() + return torch.from_numpy(self.apply_transformation(template)).float() + +class DeepGMRTransform: + def __init__(self, angle_range=45, translation_range=1): + self.angle_range = angle_range*(np.pi/180) + self.translation_range = translation_range + self.index = 0 + + def generate_transform(self): + self.anglex = np.random.uniform() * self.angle_range + self.angley = np.random.uniform() * self.angle_range + self.anglez = np.random.uniform() * self.angle_range + self.translation = np.array([np.random.uniform(-self.translation_range, self.translation_range), + np.random.uniform(-self.translation_range, self.translation_range), + np.random.uniform(-self.translation_range, self.translation_range)]) + + def apply_transformation(self, template): + rotation = Rotation.from_euler('zyx', [self.anglez, self.angley, self.anglex]) + self.igt = rotation.apply(np.eye(3)) + self.igt = np.concatenate([self.igt, self.translation.reshape(-1,1)], axis=1) + self.igt = torch.from_numpy(np.concatenate([self.igt, np.array([[0., 0., 0., 1.]])], axis=0)).float() + source = rotation.apply(template) + np.expand_dims(self.translation, axis=0) + return source + + def __call__(self, template): + template = template.numpy() + self.generate_transform() + return torch.from_numpy(self.apply_transformation(template)).float() \ No newline at end of file diff --git a/thirdparty/learning3d/pretrained/exp_classifier/events.out.tfevents.1583956166.bioroboticslab b/thirdparty/learning3d/pretrained/exp_classifier/events.out.tfevents.1583956166.bioroboticslab new file mode 100644 index 0000000000000000000000000000000000000000..e47399ef9c9a6aa351532e82678687383017fed8 --- /dev/null +++ b/thirdparty/learning3d/pretrained/exp_classifier/events.out.tfevents.1583956166.bioroboticslab @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bf8d90e91f77e3374062a9fe801ad90cd116f8f63b7f0d4e2129ea4f34c95f91 +size 49385 diff --git a/thirdparty/learning3d/pretrained/exp_classifier/models/best_model.t7 b/thirdparty/learning3d/pretrained/exp_classifier/models/best_model.t7 new file mode 100644 index 0000000000000000000000000000000000000000..fd7a09e0f98cde18b683dbe01227af6d84e31c94 --- /dev/null +++ b/thirdparty/learning3d/pretrained/exp_classifier/models/best_model.t7 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ab8d57bbea77e06bfebd64bc11343ed01621977fb0c5353e3bcd55e943710db +size 3303311 diff --git a/thirdparty/learning3d/pretrained/exp_classifier/models/best_model_snap.t7 b/thirdparty/learning3d/pretrained/exp_classifier/models/best_model_snap.t7 new file mode 100644 index 0000000000000000000000000000000000000000..328676375be56516802cca81abe073a1b233126e --- /dev/null +++ b/thirdparty/learning3d/pretrained/exp_classifier/models/best_model_snap.t7 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e95354b8b15eaf3ec7110d152258431911862581c2a1b599ca187758e1fe5f9 +size 9868985 diff --git a/thirdparty/learning3d/pretrained/exp_classifier/models/best_ptnet_model.t7 b/thirdparty/learning3d/pretrained/exp_classifier/models/best_ptnet_model.t7 new file mode 100644 index 0000000000000000000000000000000000000000..d714d1509544fb6cacbecd7bcfec2a43177ec841 --- /dev/null +++ b/thirdparty/learning3d/pretrained/exp_classifier/models/best_ptnet_model.t7 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:90ef1479ff4fa60417c226405ec687cc04e668cd2c09e669acd29301fc969af3 +size 622225 diff --git a/thirdparty/learning3d/pretrained/exp_classifier/run.log b/thirdparty/learning3d/pretrained/exp_classifier/run.log new file mode 100644 index 0000000000000000000000000000000000000000..31f9aba29edb0906c4227302fa35c4126ca238b9 --- /dev/null +++ b/thirdparty/learning3d/pretrained/exp_classifier/run.log @@ -0,0 +1,402 @@ +Namespace(batch_size=32, dataset_path='/home/vinit/vinit/software_stack/test_learning3d/../../ModelNet40/ModelNet40', dataset_type='modelnet', device='cuda:0', emb_dims=1024, epochs=200, eval=False, exp_name='exp_classifier', num_points=1024, optimizer='Adam', pointnet='tune', pretrained='', resume='', seed=1234, start_epoch=0, symfn='max', workers=4) +Namespace(batch_size=32, dataset_path='/home/vinit/vinit/software_stack/test_learning3d/../../ModelNet40/ModelNet40', dataset_type='modelnet', device='cuda:0', emb_dims=1024, epochs=200, eval=False, exp_name='exp_classifier', num_points=1024, optimizer='Adam', pointnet='tune', pretrained='', resume='', seed=1234, start_epoch=0, symfn='max', workers=4) +EPOCH:: 1, Traininig Loss: 2.059144, Testing Loss: 1.422475, Best Loss: 1.422475 +EPOCH:: 1, Traininig Accuracy: 0.469564, Testing Accuracy: 0.602917 +EPOCH:: 2, Traininig Loss: 1.304458, Testing Loss: 1.000426, Best Loss: 1.000426 +EPOCH:: 2, Traininig Accuracy: 0.636706, Testing Accuracy: 0.717180 +EPOCH:: 3, Traininig Loss: 1.120884, Testing Loss: 0.837130, Best Loss: 0.837130 +EPOCH:: 3, Traininig Accuracy: 0.679357, Testing Accuracy: 0.765397 +EPOCH:: 4, Traininig Loss: 0.987657, Testing Loss: 0.704342, Best Loss: 0.704342 +EPOCH:: 4, Traininig Accuracy: 0.717223, Testing Accuracy: 0.797812 +EPOCH:: 5, Traininig Loss: 0.925884, Testing Loss: 0.692917, Best Loss: 0.692917 +EPOCH:: 5, Traininig Accuracy: 0.729947, Testing Accuracy: 0.789303 +EPOCH:: 6, Traininig Loss: 0.881136, Testing Loss: 0.663255, Best Loss: 0.663255 +EPOCH:: 6, Traininig Accuracy: 0.742976, Testing Accuracy: 0.797812 +EPOCH:: 7, Traininig Loss: 0.840746, Testing Loss: 0.665768, Best Loss: 0.663255 +EPOCH:: 7, Traininig Accuracy: 0.752036, Testing Accuracy: 0.791734 +EPOCH:: 8, Traininig Loss: 0.809977, Testing Loss: 0.614515, Best Loss: 0.614515 +EPOCH:: 8, Traininig Accuracy: 0.763945, Testing Accuracy: 0.817261 +EPOCH:: 9, Traininig Loss: 0.767792, Testing Loss: 0.600474, Best Loss: 0.600474 +EPOCH:: 9, Traininig Accuracy: 0.775855, Testing Accuracy: 0.815235 +EPOCH:: 10, Traininig Loss: 0.728459, Testing Loss: 0.577109, Best Loss: 0.577109 +EPOCH:: 10, Traininig Accuracy: 0.787256, Testing Accuracy: 0.824149 +EPOCH:: 11, Traininig Loss: 0.724967, Testing Loss: 0.569139, Best Loss: 0.569139 +EPOCH:: 11, Traininig Accuracy: 0.785322, Testing Accuracy: 0.823339 +EPOCH:: 12, Traininig Loss: 0.698121, Testing Loss: 0.561024, Best Loss: 0.561024 +EPOCH:: 12, Traininig Accuracy: 0.793974, Testing Accuracy: 0.833063 +EPOCH:: 13, Traininig Loss: 0.677532, Testing Loss: 0.538425, Best Loss: 0.538425 +EPOCH:: 13, Traininig Accuracy: 0.798758, Testing Accuracy: 0.833874 +EPOCH:: 14, Traininig Loss: 0.666194, Testing Loss: 0.522871, Best Loss: 0.522871 +EPOCH:: 14, Traininig Accuracy: 0.802524, Testing Accuracy: 0.843193 +EPOCH:: 15, Traininig Loss: 0.638233, Testing Loss: 0.543664, Best Loss: 0.522871 +EPOCH:: 15, Traininig Accuracy: 0.806596, Testing Accuracy: 0.836305 +EPOCH:: 16, Traininig Loss: 0.645889, Testing Loss: 0.516796, Best Loss: 0.516796 +EPOCH:: 16, Traininig Accuracy: 0.806189, Testing Accuracy: 0.842788 +EPOCH:: 17, Traininig Loss: 0.619007, Testing Loss: 0.585809, Best Loss: 0.516796 +EPOCH:: 17, Traininig Accuracy: 0.814536, Testing Accuracy: 0.825770 +EPOCH:: 18, Traininig Loss: 0.626973, Testing Loss: 0.491648, Best Loss: 0.491648 +EPOCH:: 18, Traininig Accuracy: 0.807716, Testing Accuracy: 0.850081 +EPOCH:: 19, Traininig Loss: 0.611081, Testing Loss: 0.537293, Best Loss: 0.491648 +EPOCH:: 19, Traininig Accuracy: 0.816368, Testing Accuracy: 0.842382 +EPOCH:: 20, Traininig Loss: 0.601878, Testing Loss: 0.524312, Best Loss: 0.491648 +EPOCH:: 20, Traininig Accuracy: 0.818200, Testing Accuracy: 0.840357 +EPOCH:: 21, Traininig Loss: 0.575580, Testing Loss: 0.484350, Best Loss: 0.484350 +EPOCH:: 21, Traininig Accuracy: 0.821458, Testing Accuracy: 0.854133 +EPOCH:: 22, Traininig Loss: 0.564111, Testing Loss: 0.517002, Best Loss: 0.484350 +EPOCH:: 22, Traininig Accuracy: 0.824715, Testing Accuracy: 0.841572 +EPOCH:: 23, Traininig Loss: 0.565972, Testing Loss: 0.515537, Best Loss: 0.484350 +EPOCH:: 23, Traininig Accuracy: 0.827463, Testing Accuracy: 0.846840 +EPOCH:: 24, Traininig Loss: 0.547577, Testing Loss: 0.486071, Best Loss: 0.484350 +EPOCH:: 24, Traininig Accuracy: 0.829397, Testing Accuracy: 0.844814 +EPOCH:: 25, Traininig Loss: 0.552742, Testing Loss: 0.483412, Best Loss: 0.483412 +EPOCH:: 25, Traininig Accuracy: 0.832960, Testing Accuracy: 0.854538 +EPOCH:: 26, Traininig Loss: 0.547706, Testing Loss: 0.511354, Best Loss: 0.483412 +EPOCH:: 26, Traininig Accuracy: 0.831230, Testing Accuracy: 0.844003 +EPOCH:: 27, Traininig Loss: 0.538499, Testing Loss: 0.519035, Best Loss: 0.483412 +EPOCH:: 27, Traininig Accuracy: 0.835607, Testing Accuracy: 0.848460 +EPOCH:: 28, Traininig Loss: 0.532696, Testing Loss: 0.485843, Best Loss: 0.483412 +EPOCH:: 28, Traininig Accuracy: 0.835810, Testing Accuracy: 0.850081 +EPOCH:: 29, Traininig Loss: 0.513994, Testing Loss: 0.483129, Best Loss: 0.483129 +EPOCH:: 29, Traininig Accuracy: 0.839577, Testing Accuracy: 0.853323 +EPOCH:: 30, Traininig Loss: 0.513041, Testing Loss: 0.490930, Best Loss: 0.483129 +EPOCH:: 30, Traininig Accuracy: 0.839678, Testing Accuracy: 0.849271 +EPOCH:: 31, Traininig Loss: 0.501132, Testing Loss: 0.480702, Best Loss: 0.480702 +EPOCH:: 31, Traininig Accuracy: 0.844971, Testing Accuracy: 0.858185 +EPOCH:: 32, Traininig Loss: 0.512641, Testing Loss: 0.454187, Best Loss: 0.454187 +EPOCH:: 32, Traininig Accuracy: 0.844259, Testing Accuracy: 0.863452 +EPOCH:: 33, Traininig Loss: 0.484405, Testing Loss: 0.513030, Best Loss: 0.454187 +EPOCH:: 33, Traininig Accuracy: 0.847923, Testing Accuracy: 0.848865 +EPOCH:: 34, Traininig Loss: 0.489131, Testing Loss: 0.479277, Best Loss: 0.454187 +EPOCH:: 34, Traininig Accuracy: 0.843037, Testing Accuracy: 0.848055 +EPOCH:: 35, Traininig Loss: 0.455944, Testing Loss: 0.453138, Best Loss: 0.453138 +EPOCH:: 35, Traininig Accuracy: 0.859528, Testing Accuracy: 0.861426 +EPOCH:: 36, Traininig Loss: 0.466452, Testing Loss: 0.440202, Best Loss: 0.440202 +EPOCH:: 36, Traininig Accuracy: 0.856372, Testing Accuracy: 0.861831 +EPOCH:: 37, Traininig Loss: 0.469649, Testing Loss: 0.458507, Best Loss: 0.440202 +EPOCH:: 37, Traininig Accuracy: 0.852097, Testing Accuracy: 0.854538 +EPOCH:: 38, Traininig Loss: 0.455056, Testing Loss: 0.474200, Best Loss: 0.440202 +EPOCH:: 38, Traininig Accuracy: 0.859528, Testing Accuracy: 0.857780 +EPOCH:: 39, Traininig Loss: 0.457084, Testing Loss: 0.451106, Best Loss: 0.440202 +EPOCH:: 39, Traininig Accuracy: 0.860240, Testing Accuracy: 0.861426 +EPOCH:: 40, Traininig Loss: 0.456032, Testing Loss: 0.479609, Best Loss: 0.440202 +EPOCH:: 40, Traininig Accuracy: 0.859324, Testing Accuracy: 0.856969 +EPOCH:: 41, Traininig Loss: 0.451229, Testing Loss: 0.468210, Best Loss: 0.440202 +EPOCH:: 41, Traininig Accuracy: 0.854947, Testing Accuracy: 0.856564 +EPOCH:: 42, Traininig Loss: 0.445853, Testing Loss: 0.440681, Best Loss: 0.440202 +EPOCH:: 42, Traininig Accuracy: 0.863090, Testing Accuracy: 0.865883 +EPOCH:: 43, Traininig Loss: 0.448099, Testing Loss: 0.466853, Best Loss: 0.440202 +EPOCH:: 43, Traininig Accuracy: 0.858204, Testing Accuracy: 0.861426 +EPOCH:: 44, Traininig Loss: 0.438799, Testing Loss: 0.470048, Best Loss: 0.440202 +EPOCH:: 44, Traininig Accuracy: 0.859935, Testing Accuracy: 0.866694 +EPOCH:: 45, Traininig Loss: 0.419709, Testing Loss: 0.439138, Best Loss: 0.439138 +EPOCH:: 45, Traininig Accuracy: 0.869503, Testing Accuracy: 0.872366 +EPOCH:: 46, Traininig Loss: 0.418666, Testing Loss: 0.425229, Best Loss: 0.425229 +EPOCH:: 46, Traininig Accuracy: 0.869707, Testing Accuracy: 0.875608 +EPOCH:: 47, Traininig Loss: 0.415427, Testing Loss: 0.445391, Best Loss: 0.425229 +EPOCH:: 47, Traininig Accuracy: 0.868078, Testing Accuracy: 0.873582 +EPOCH:: 48, Traininig Loss: 0.414733, Testing Loss: 0.460479, Best Loss: 0.425229 +EPOCH:: 48, Traininig Accuracy: 0.866042, Testing Accuracy: 0.861426 +EPOCH:: 49, Traininig Loss: 0.433628, Testing Loss: 0.438064, Best Loss: 0.425229 +EPOCH:: 49, Traininig Accuracy: 0.862581, Testing Accuracy: 0.864263 +EPOCH:: 50, Traininig Loss: 0.408090, Testing Loss: 0.454362, Best Loss: 0.425229 +EPOCH:: 50, Traininig Accuracy: 0.869300, Testing Accuracy: 0.865073 +EPOCH:: 51, Traininig Loss: 0.403481, Testing Loss: 0.466190, Best Loss: 0.425229 +EPOCH:: 51, Traininig Accuracy: 0.874287, Testing Accuracy: 0.865073 +EPOCH:: 52, Traininig Loss: 0.406790, Testing Loss: 0.452115, Best Loss: 0.425229 +EPOCH:: 52, Traininig Accuracy: 0.871234, Testing Accuracy: 0.865883 +EPOCH:: 53, Traininig Loss: 0.401439, Testing Loss: 0.430931, Best Loss: 0.425229 +EPOCH:: 53, Traininig Accuracy: 0.872862, Testing Accuracy: 0.879254 +EPOCH:: 54, Traininig Loss: 0.392144, Testing Loss: 0.442428, Best Loss: 0.425229 +EPOCH:: 54, Traininig Accuracy: 0.874796, Testing Accuracy: 0.871556 +EPOCH:: 55, Traininig Loss: 0.403287, Testing Loss: 0.442578, Best Loss: 0.425229 +EPOCH:: 55, Traininig Accuracy: 0.874491, Testing Accuracy: 0.874392 +EPOCH:: 56, Traininig Loss: 0.393099, Testing Loss: 0.442935, Best Loss: 0.425229 +EPOCH:: 56, Traininig Accuracy: 0.877239, Testing Accuracy: 0.871151 +EPOCH:: 57, Traininig Loss: 0.379587, Testing Loss: 0.440191, Best Loss: 0.425229 +EPOCH:: 57, Traininig Accuracy: 0.878359, Testing Accuracy: 0.873987 +EPOCH:: 58, Traininig Loss: 0.385819, Testing Loss: 0.422093, Best Loss: 0.422093 +EPOCH:: 58, Traininig Accuracy: 0.876221, Testing Accuracy: 0.879660 +EPOCH:: 59, Traininig Loss: 0.390624, Testing Loss: 0.473527, Best Loss: 0.422093 +EPOCH:: 59, Traininig Accuracy: 0.875000, Testing Accuracy: 0.867504 +EPOCH:: 60, Traininig Loss: 0.389040, Testing Loss: 0.444967, Best Loss: 0.422093 +EPOCH:: 60, Traininig Accuracy: 0.876323, Testing Accuracy: 0.876418 +EPOCH:: 61, Traininig Loss: 0.380482, Testing Loss: 0.500552, Best Loss: 0.422093 +EPOCH:: 61, Traininig Accuracy: 0.879275, Testing Accuracy: 0.854133 +EPOCH:: 62, Traininig Loss: 0.377065, Testing Loss: 0.440564, Best Loss: 0.422093 +EPOCH:: 62, Traininig Accuracy: 0.878257, Testing Accuracy: 0.867909 +EPOCH:: 63, Traininig Loss: 0.373190, Testing Loss: 0.451005, Best Loss: 0.422093 +EPOCH:: 63, Traininig Accuracy: 0.877647, Testing Accuracy: 0.878039 +EPOCH:: 64, Traininig Loss: 0.352867, Testing Loss: 0.440301, Best Loss: 0.422093 +EPOCH:: 64, Traininig Accuracy: 0.882533, Testing Accuracy: 0.874797 +EPOCH:: 65, Traininig Loss: 0.358123, Testing Loss: 0.441810, Best Loss: 0.422093 +EPOCH:: 65, Traininig Accuracy: 0.885383, Testing Accuracy: 0.877634 +EPOCH:: 66, Traininig Loss: 0.358309, Testing Loss: 0.444104, Best Loss: 0.422093 +EPOCH:: 66, Traininig Accuracy: 0.886401, Testing Accuracy: 0.876418 +EPOCH:: 67, Traininig Loss: 0.360357, Testing Loss: 0.454441, Best Loss: 0.422093 +EPOCH:: 67, Traininig Accuracy: 0.884670, Testing Accuracy: 0.867504 +EPOCH:: 68, Traininig Loss: 0.355257, Testing Loss: 0.448909, Best Loss: 0.422093 +EPOCH:: 68, Traininig Accuracy: 0.886197, Testing Accuracy: 0.871151 +EPOCH:: 69, Traininig Loss: 0.352099, Testing Loss: 0.458359, Best Loss: 0.422093 +EPOCH:: 69, Traininig Accuracy: 0.883245, Testing Accuracy: 0.868720 +EPOCH:: 70, Traininig Loss: 0.344170, Testing Loss: 0.441043, Best Loss: 0.422093 +EPOCH:: 70, Traininig Accuracy: 0.889251, Testing Accuracy: 0.877634 +EPOCH:: 71, Traininig Loss: 0.349346, Testing Loss: 0.477251, Best Loss: 0.422093 +EPOCH:: 71, Traininig Accuracy: 0.885179, Testing Accuracy: 0.854133 +EPOCH:: 72, Traininig Loss: 0.355714, Testing Loss: 0.431372, Best Loss: 0.422093 +EPOCH:: 72, Traininig Accuracy: 0.884467, Testing Accuracy: 0.882091 +EPOCH:: 73, Traininig Loss: 0.345623, Testing Loss: 0.448816, Best Loss: 0.422093 +EPOCH:: 73, Traininig Accuracy: 0.886401, Testing Accuracy: 0.868720 +EPOCH:: 74, Traininig Loss: 0.340500, Testing Loss: 0.459223, Best Loss: 0.422093 +EPOCH:: 74, Traininig Accuracy: 0.889047, Testing Accuracy: 0.878039 +EPOCH:: 75, Traininig Loss: 0.349159, Testing Loss: 0.454139, Best Loss: 0.422093 +EPOCH:: 75, Traininig Accuracy: 0.888335, Testing Accuracy: 0.873582 +EPOCH:: 76, Traininig Loss: 0.328940, Testing Loss: 0.474430, Best Loss: 0.422093 +EPOCH:: 76, Traininig Accuracy: 0.890167, Testing Accuracy: 0.865883 +EPOCH:: 77, Traininig Loss: 0.319349, Testing Loss: 0.495119, Best Loss: 0.422093 +EPOCH:: 77, Traininig Accuracy: 0.893322, Testing Accuracy: 0.869530 +EPOCH:: 78, Traininig Loss: 0.328202, Testing Loss: 0.430176, Best Loss: 0.422093 +EPOCH:: 78, Traininig Accuracy: 0.896580, Testing Accuracy: 0.878444 +EPOCH:: 79, Traininig Loss: 0.305153, Testing Loss: 0.429587, Best Loss: 0.422093 +EPOCH:: 79, Traininig Accuracy: 0.899125, Testing Accuracy: 0.872366 +EPOCH:: 80, Traininig Loss: 0.324374, Testing Loss: 0.486990, Best Loss: 0.422093 +EPOCH:: 80, Traininig Accuracy: 0.895765, Testing Accuracy: 0.864263 +EPOCH:: 81, Traininig Loss: 0.327259, Testing Loss: 0.484366, Best Loss: 0.422093 +EPOCH:: 81, Traininig Accuracy: 0.892101, Testing Accuracy: 0.865478 +EPOCH:: 82, Traininig Loss: 0.320915, Testing Loss: 0.465965, Best Loss: 0.422093 +EPOCH:: 82, Traininig Accuracy: 0.894035, Testing Accuracy: 0.876013 +EPOCH:: 83, Traininig Loss: 0.314916, Testing Loss: 0.479534, Best Loss: 0.422093 +EPOCH:: 83, Traininig Accuracy: 0.892203, Testing Accuracy: 0.870746 +EPOCH:: 84, Traininig Loss: 0.316541, Testing Loss: 0.454629, Best Loss: 0.422093 +EPOCH:: 84, Traininig Accuracy: 0.898412, Testing Accuracy: 0.869935 +EPOCH:: 85, Traininig Loss: 0.314456, Testing Loss: 0.498205, Best Loss: 0.422093 +EPOCH:: 85, Traininig Accuracy: 0.893119, Testing Accuracy: 0.875203 +EPOCH:: 86, Traininig Loss: 0.314816, Testing Loss: 0.454265, Best Loss: 0.422093 +EPOCH:: 86, Traininig Accuracy: 0.899532, Testing Accuracy: 0.880470 +EPOCH:: 87, Traininig Loss: 0.314459, Testing Loss: 0.453104, Best Loss: 0.422093 +EPOCH:: 87, Traininig Accuracy: 0.895867, Testing Accuracy: 0.878444 +EPOCH:: 88, Traininig Loss: 0.328017, Testing Loss: 0.453576, Best Loss: 0.422093 +EPOCH:: 88, Traininig Accuracy: 0.895257, Testing Accuracy: 0.872366 +EPOCH:: 89, Traininig Loss: 0.314356, Testing Loss: 0.459951, Best Loss: 0.422093 +EPOCH:: 89, Traininig Accuracy: 0.897292, Testing Accuracy: 0.869125 +EPOCH:: 90, Traininig Loss: 0.306303, Testing Loss: 0.455191, Best Loss: 0.422093 +EPOCH:: 90, Traininig Accuracy: 0.899023, Testing Accuracy: 0.877229 +EPOCH:: 91, Traininig Loss: 0.305995, Testing Loss: 0.452759, Best Loss: 0.422093 +EPOCH:: 91, Traininig Accuracy: 0.895969, Testing Accuracy: 0.878444 +EPOCH:: 92, Traininig Loss: 0.292685, Testing Loss: 0.467052, Best Loss: 0.422093 +EPOCH:: 92, Traininig Accuracy: 0.902789, Testing Accuracy: 0.874392 +EPOCH:: 93, Traininig Loss: 0.302490, Testing Loss: 0.470551, Best Loss: 0.422093 +EPOCH:: 93, Traininig Accuracy: 0.898005, Testing Accuracy: 0.879660 +EPOCH:: 94, Traininig Loss: 0.309826, Testing Loss: 0.471789, Best Loss: 0.422093 +EPOCH:: 94, Traininig Accuracy: 0.898107, Testing Accuracy: 0.868314 +EPOCH:: 95, Traininig Loss: 0.293325, Testing Loss: 0.471290, Best Loss: 0.422093 +EPOCH:: 95, Traininig Accuracy: 0.901568, Testing Accuracy: 0.870340 +EPOCH:: 96, Traininig Loss: 0.300629, Testing Loss: 0.451013, Best Loss: 0.422093 +EPOCH:: 96, Traininig Accuracy: 0.900143, Testing Accuracy: 0.881686 +EPOCH:: 97, Traininig Loss: 0.291577, Testing Loss: 0.482157, Best Loss: 0.422093 +EPOCH:: 97, Traininig Accuracy: 0.901262, Testing Accuracy: 0.875203 +EPOCH:: 98, Traininig Loss: 0.300759, Testing Loss: 0.474427, Best Loss: 0.422093 +EPOCH:: 98, Traininig Accuracy: 0.900041, Testing Accuracy: 0.877634 +EPOCH:: 99, Traininig Loss: 0.303200, Testing Loss: 0.469732, Best Loss: 0.422093 +EPOCH:: 99, Traininig Accuracy: 0.900143, Testing Accuracy: 0.874797 +EPOCH:: 100, Traininig Loss: 0.293520, Testing Loss: 0.478255, Best Loss: 0.422093 +EPOCH:: 100, Traininig Accuracy: 0.904520, Testing Accuracy: 0.874392 +EPOCH:: 101, Traininig Loss: 0.302843, Testing Loss: 0.462872, Best Loss: 0.422093 +EPOCH:: 101, Traininig Accuracy: 0.904011, Testing Accuracy: 0.879254 +EPOCH:: 102, Traininig Loss: 0.290085, Testing Loss: 0.455734, Best Loss: 0.422093 +EPOCH:: 102, Traininig Accuracy: 0.904214, Testing Accuracy: 0.884117 +EPOCH:: 103, Traininig Loss: 0.287412, Testing Loss: 0.485150, Best Loss: 0.422093 +EPOCH:: 103, Traininig Accuracy: 0.905945, Testing Accuracy: 0.880470 +EPOCH:: 104, Traininig Loss: 0.272377, Testing Loss: 0.510814, Best Loss: 0.422093 +EPOCH:: 104, Traininig Accuracy: 0.908795, Testing Accuracy: 0.873582 +EPOCH:: 105, Traininig Loss: 0.282540, Testing Loss: 0.466367, Best Loss: 0.422093 +EPOCH:: 105, Traininig Accuracy: 0.905945, Testing Accuracy: 0.878039 +EPOCH:: 106, Traininig Loss: 0.280139, Testing Loss: 0.458955, Best Loss: 0.422093 +EPOCH:: 106, Traininig Accuracy: 0.906046, Testing Accuracy: 0.877634 +EPOCH:: 107, Traininig Loss: 0.278995, Testing Loss: 0.473659, Best Loss: 0.422093 +EPOCH:: 107, Traininig Accuracy: 0.905945, Testing Accuracy: 0.879660 +EPOCH:: 108, Traininig Loss: 0.273601, Testing Loss: 0.533686, Best Loss: 0.422093 +EPOCH:: 108, Traininig Accuracy: 0.908489, Testing Accuracy: 0.872366 +EPOCH:: 109, Traininig Loss: 0.264263, Testing Loss: 0.486138, Best Loss: 0.422093 +EPOCH:: 109, Traininig Accuracy: 0.914699, Testing Accuracy: 0.873177 +EPOCH:: 110, Traininig Loss: 0.287907, Testing Loss: 0.492506, Best Loss: 0.422093 +EPOCH:: 110, Traininig Accuracy: 0.906759, Testing Accuracy: 0.878039 +EPOCH:: 111, Traininig Loss: 0.287943, Testing Loss: 0.489832, Best Loss: 0.422093 +EPOCH:: 111, Traininig Accuracy: 0.902789, Testing Accuracy: 0.874797 +EPOCH:: 112, Traininig Loss: 0.294778, Testing Loss: 0.474281, Best Loss: 0.422093 +EPOCH:: 112, Traininig Accuracy: 0.902993, Testing Accuracy: 0.882091 +EPOCH:: 113, Traininig Loss: 0.272014, Testing Loss: 0.497284, Best Loss: 0.422093 +EPOCH:: 113, Traininig Accuracy: 0.909202, Testing Accuracy: 0.872771 +EPOCH:: 114, Traininig Loss: 0.264365, Testing Loss: 0.447196, Best Loss: 0.422093 +EPOCH:: 114, Traininig Accuracy: 0.913172, Testing Accuracy: 0.882901 +EPOCH:: 115, Traininig Loss: 0.263099, Testing Loss: 0.454236, Best Loss: 0.422093 +EPOCH:: 115, Traininig Accuracy: 0.912459, Testing Accuracy: 0.877634 +EPOCH:: 116, Traininig Loss: 0.268953, Testing Loss: 0.539014, Best Loss: 0.422093 +EPOCH:: 116, Traininig Accuracy: 0.909914, Testing Accuracy: 0.865883 +EPOCH:: 117, Traininig Loss: 0.276246, Testing Loss: 0.461685, Best Loss: 0.422093 +EPOCH:: 117, Traininig Accuracy: 0.908795, Testing Accuracy: 0.884927 +EPOCH:: 118, Traininig Loss: 0.257859, Testing Loss: 0.464540, Best Loss: 0.422093 +EPOCH:: 118, Traininig Accuracy: 0.914292, Testing Accuracy: 0.875203 +EPOCH:: 119, Traininig Loss: 0.259185, Testing Loss: 0.497717, Best Loss: 0.422093 +EPOCH:: 119, Traininig Accuracy: 0.914597, Testing Accuracy: 0.876823 +EPOCH:: 120, Traininig Loss: 0.250828, Testing Loss: 0.456011, Best Loss: 0.422093 +EPOCH:: 120, Traininig Accuracy: 0.914190, Testing Accuracy: 0.884117 +EPOCH:: 121, Traininig Loss: 0.268490, Testing Loss: 0.518518, Best Loss: 0.422093 +EPOCH:: 121, Traininig Accuracy: 0.908286, Testing Accuracy: 0.878849 +EPOCH:: 122, Traininig Loss: 0.255576, Testing Loss: 0.529505, Best Loss: 0.422093 +EPOCH:: 122, Traininig Accuracy: 0.916429, Testing Accuracy: 0.879660 +EPOCH:: 123, Traininig Loss: 0.265231, Testing Loss: 0.510548, Best Loss: 0.422093 +EPOCH:: 123, Traininig Accuracy: 0.910423, Testing Accuracy: 0.870340 +EPOCH:: 124, Traininig Loss: 0.264802, Testing Loss: 0.508521, Best Loss: 0.422093 +EPOCH:: 124, Traininig Accuracy: 0.913783, Testing Accuracy: 0.873177 +EPOCH:: 125, Traininig Loss: 0.254298, Testing Loss: 0.457226, Best Loss: 0.422093 +EPOCH:: 125, Traininig Accuracy: 0.916226, Testing Accuracy: 0.886143 +EPOCH:: 126, Traininig Loss: 0.254651, Testing Loss: 0.477440, Best Loss: 0.422093 +EPOCH:: 126, Traininig Accuracy: 0.911136, Testing Accuracy: 0.879660 +EPOCH:: 127, Traininig Loss: 0.253943, Testing Loss: 0.475400, Best Loss: 0.422093 +EPOCH:: 127, Traininig Accuracy: 0.917040, Testing Accuracy: 0.873582 +EPOCH:: 128, Traininig Loss: 0.254880, Testing Loss: 0.462656, Best Loss: 0.422093 +EPOCH:: 128, Traininig Accuracy: 0.918567, Testing Accuracy: 0.883306 +EPOCH:: 129, Traininig Loss: 0.251518, Testing Loss: 0.503920, Best Loss: 0.422093 +EPOCH:: 129, Traininig Accuracy: 0.915004, Testing Accuracy: 0.879254 +EPOCH:: 130, Traininig Loss: 0.232400, Testing Loss: 0.480314, Best Loss: 0.422093 +EPOCH:: 130, Traininig Accuracy: 0.921213, Testing Accuracy: 0.877634 +EPOCH:: 131, Traininig Loss: 0.243682, Testing Loss: 0.571570, Best Loss: 0.422093 +EPOCH:: 131, Traininig Accuracy: 0.919279, Testing Accuracy: 0.870340 +EPOCH:: 132, Traininig Loss: 0.248767, Testing Loss: 0.490813, Best Loss: 0.422093 +EPOCH:: 132, Traininig Accuracy: 0.915615, Testing Accuracy: 0.876013 +EPOCH:: 133, Traininig Loss: 0.255036, Testing Loss: 0.520035, Best Loss: 0.422093 +EPOCH:: 133, Traininig Accuracy: 0.914292, Testing Accuracy: 0.876013 +EPOCH:: 134, Traininig Loss: 0.248792, Testing Loss: 0.466571, Best Loss: 0.422093 +EPOCH:: 134, Traininig Accuracy: 0.916735, Testing Accuracy: 0.878444 +EPOCH:: 135, Traininig Loss: 0.239071, Testing Loss: 0.471038, Best Loss: 0.422093 +EPOCH:: 135, Traininig Accuracy: 0.916735, Testing Accuracy: 0.877229 +EPOCH:: 136, Traininig Loss: 0.248556, Testing Loss: 0.513943, Best Loss: 0.422093 +EPOCH:: 136, Traininig Accuracy: 0.917956, Testing Accuracy: 0.873987 +EPOCH:: 137, Traininig Loss: 0.236882, Testing Loss: 0.537659, Best Loss: 0.422093 +EPOCH:: 137, Traininig Accuracy: 0.921926, Testing Accuracy: 0.865478 +EPOCH:: 138, Traininig Loss: 0.238876, Testing Loss: 0.493314, Best Loss: 0.422093 +EPOCH:: 138, Traininig Accuracy: 0.918974, Testing Accuracy: 0.879254 +EPOCH:: 139, Traininig Loss: 0.251112, Testing Loss: 0.502006, Best Loss: 0.422093 +EPOCH:: 139, Traininig Accuracy: 0.916633, Testing Accuracy: 0.873582 +EPOCH:: 140, Traininig Loss: 0.236773, Testing Loss: 0.475554, Best Loss: 0.422093 +EPOCH:: 140, Traininig Accuracy: 0.918669, Testing Accuracy: 0.882901 +EPOCH:: 141, Traininig Loss: 0.231244, Testing Loss: 0.508512, Best Loss: 0.422093 +EPOCH:: 141, Traininig Accuracy: 0.920908, Testing Accuracy: 0.878849 +EPOCH:: 142, Traininig Loss: 0.223129, Testing Loss: 0.490020, Best Loss: 0.422093 +EPOCH:: 142, Traininig Accuracy: 0.923249, Testing Accuracy: 0.876418 +EPOCH:: 143, Traininig Loss: 0.232410, Testing Loss: 0.480369, Best Loss: 0.422093 +EPOCH:: 143, Traininig Accuracy: 0.919788, Testing Accuracy: 0.884927 +EPOCH:: 144, Traininig Loss: 0.240110, Testing Loss: 0.499966, Best Loss: 0.422093 +EPOCH:: 144, Traininig Accuracy: 0.918770, Testing Accuracy: 0.875203 +EPOCH:: 145, Traininig Loss: 0.233934, Testing Loss: 0.516689, Best Loss: 0.422093 +EPOCH:: 145, Traininig Accuracy: 0.925590, Testing Accuracy: 0.881280 +EPOCH:: 146, Traininig Loss: 0.221208, Testing Loss: 0.504159, Best Loss: 0.422093 +EPOCH:: 146, Traininig Accuracy: 0.926303, Testing Accuracy: 0.880875 +EPOCH:: 147, Traininig Loss: 0.232562, Testing Loss: 0.462673, Best Loss: 0.422093 +EPOCH:: 147, Traininig Accuracy: 0.921010, Testing Accuracy: 0.885737 +EPOCH:: 148, Traininig Loss: 0.228521, Testing Loss: 0.487295, Best Loss: 0.422093 +EPOCH:: 148, Traininig Accuracy: 0.924674, Testing Accuracy: 0.880875 +EPOCH:: 149, Traininig Loss: 0.234641, Testing Loss: 0.500212, Best Loss: 0.422093 +EPOCH:: 149, Traininig Accuracy: 0.922944, Testing Accuracy: 0.875608 +EPOCH:: 150, Traininig Loss: 0.224925, Testing Loss: 0.521341, Best Loss: 0.422093 +EPOCH:: 150, Traininig Accuracy: 0.925489, Testing Accuracy: 0.877634 +EPOCH:: 151, Traininig Loss: 0.232542, Testing Loss: 0.458233, Best Loss: 0.422093 +EPOCH:: 151, Traininig Accuracy: 0.922435, Testing Accuracy: 0.882901 +EPOCH:: 152, Traininig Loss: 0.226221, Testing Loss: 0.489239, Best Loss: 0.422093 +EPOCH:: 152, Traininig Accuracy: 0.923860, Testing Accuracy: 0.879254 +EPOCH:: 153, Traininig Loss: 0.230152, Testing Loss: 0.496815, Best Loss: 0.422093 +EPOCH:: 153, Traininig Accuracy: 0.926608, Testing Accuracy: 0.884117 +EPOCH:: 154, Traininig Loss: 0.222743, Testing Loss: 0.495687, Best Loss: 0.422093 +EPOCH:: 154, Traininig Accuracy: 0.925590, Testing Accuracy: 0.882901 +EPOCH:: 155, Traininig Loss: 0.221276, Testing Loss: 0.478790, Best Loss: 0.422093 +EPOCH:: 155, Traininig Accuracy: 0.924369, Testing Accuracy: 0.877634 +EPOCH:: 156, Traininig Loss: 0.224296, Testing Loss: 0.544537, Best Loss: 0.422093 +EPOCH:: 156, Traininig Accuracy: 0.925285, Testing Accuracy: 0.873582 +EPOCH:: 157, Traininig Loss: 0.224527, Testing Loss: 0.508608, Best Loss: 0.422093 +EPOCH:: 157, Traininig Accuracy: 0.924369, Testing Accuracy: 0.879254 +EPOCH:: 158, Traininig Loss: 0.222218, Testing Loss: 0.485479, Best Loss: 0.422093 +EPOCH:: 158, Traininig Accuracy: 0.926405, Testing Accuracy: 0.880875 +EPOCH:: 159, Traininig Loss: 0.214929, Testing Loss: 0.535499, Best Loss: 0.422093 +EPOCH:: 159, Traininig Accuracy: 0.926914, Testing Accuracy: 0.873582 +EPOCH:: 160, Traininig Loss: 0.218075, Testing Loss: 0.483316, Best Loss: 0.422093 +EPOCH:: 160, Traininig Accuracy: 0.926608, Testing Accuracy: 0.884117 +EPOCH:: 161, Traininig Loss: 0.223491, Testing Loss: 0.494604, Best Loss: 0.422093 +EPOCH:: 161, Traininig Accuracy: 0.924267, Testing Accuracy: 0.880470 +EPOCH:: 162, Traininig Loss: 0.213008, Testing Loss: 0.536357, Best Loss: 0.422093 +EPOCH:: 162, Traininig Accuracy: 0.927728, Testing Accuracy: 0.879660 +EPOCH:: 163, Traininig Loss: 0.212290, Testing Loss: 0.527058, Best Loss: 0.422093 +EPOCH:: 163, Traininig Accuracy: 0.929662, Testing Accuracy: 0.878039 +EPOCH:: 164, Traininig Loss: 0.222263, Testing Loss: 0.517100, Best Loss: 0.422093 +EPOCH:: 164, Traininig Accuracy: 0.927219, Testing Accuracy: 0.879254 +EPOCH:: 165, Traininig Loss: 0.217002, Testing Loss: 0.551008, Best Loss: 0.422093 +EPOCH:: 165, Traininig Accuracy: 0.927321, Testing Accuracy: 0.872771 +EPOCH:: 166, Traininig Loss: 0.208326, Testing Loss: 0.490151, Best Loss: 0.422093 +EPOCH:: 166, Traininig Accuracy: 0.929153, Testing Accuracy: 0.882496 +EPOCH:: 167, Traininig Loss: 0.215832, Testing Loss: 0.528867, Best Loss: 0.422093 +EPOCH:: 167, Traininig Accuracy: 0.928339, Testing Accuracy: 0.874797 +EPOCH:: 168, Traininig Loss: 0.210918, Testing Loss: 0.513013, Best Loss: 0.422093 +EPOCH:: 168, Traininig Accuracy: 0.926812, Testing Accuracy: 0.884117 +EPOCH:: 169, Traininig Loss: 0.214767, Testing Loss: 0.492718, Best Loss: 0.422093 +EPOCH:: 169, Traininig Accuracy: 0.929153, Testing Accuracy: 0.883712 +EPOCH:: 170, Traininig Loss: 0.208524, Testing Loss: 0.515303, Best Loss: 0.422093 +EPOCH:: 170, Traininig Accuracy: 0.930578, Testing Accuracy: 0.876013 +EPOCH:: 171, Traininig Loss: 0.211597, Testing Loss: 0.531030, Best Loss: 0.422093 +EPOCH:: 171, Traininig Accuracy: 0.927321, Testing Accuracy: 0.878444 +EPOCH:: 172, Traininig Loss: 0.207133, Testing Loss: 0.557470, Best Loss: 0.422093 +EPOCH:: 172, Traininig Accuracy: 0.933327, Testing Accuracy: 0.876823 +EPOCH:: 173, Traininig Loss: 0.206519, Testing Loss: 0.509256, Best Loss: 0.422093 +EPOCH:: 173, Traininig Accuracy: 0.931494, Testing Accuracy: 0.882091 +EPOCH:: 174, Traininig Loss: 0.211670, Testing Loss: 0.513361, Best Loss: 0.422093 +EPOCH:: 174, Traininig Accuracy: 0.925692, Testing Accuracy: 0.877634 +EPOCH:: 175, Traininig Loss: 0.209094, Testing Loss: 0.479022, Best Loss: 0.422093 +EPOCH:: 175, Traininig Accuracy: 0.929255, Testing Accuracy: 0.880470 +EPOCH:: 176, Traininig Loss: 0.208357, Testing Loss: 0.484987, Best Loss: 0.422093 +EPOCH:: 176, Traininig Accuracy: 0.932105, Testing Accuracy: 0.880875 +EPOCH:: 177, Traininig Loss: 0.201747, Testing Loss: 0.509507, Best Loss: 0.422093 +EPOCH:: 177, Traininig Accuracy: 0.931087, Testing Accuracy: 0.878849 +EPOCH:: 178, Traininig Loss: 0.196966, Testing Loss: 0.535068, Best Loss: 0.422093 +EPOCH:: 178, Traininig Accuracy: 0.933327, Testing Accuracy: 0.869125 +EPOCH:: 179, Traininig Loss: 0.194132, Testing Loss: 0.500543, Best Loss: 0.422093 +EPOCH:: 179, Traininig Accuracy: 0.932818, Testing Accuracy: 0.882091 +EPOCH:: 180, Traininig Loss: 0.213296, Testing Loss: 0.517571, Best Loss: 0.422093 +EPOCH:: 180, Traininig Accuracy: 0.931189, Testing Accuracy: 0.873582 +EPOCH:: 181, Traininig Loss: 0.219161, Testing Loss: 0.526574, Best Loss: 0.422093 +EPOCH:: 181, Traininig Accuracy: 0.927932, Testing Accuracy: 0.886548 +EPOCH:: 182, Traininig Loss: 0.190213, Testing Loss: 0.520709, Best Loss: 0.422093 +EPOCH:: 182, Traininig Accuracy: 0.935566, Testing Accuracy: 0.880875 +EPOCH:: 183, Traininig Loss: 0.200500, Testing Loss: 0.512031, Best Loss: 0.422093 +EPOCH:: 183, Traininig Accuracy: 0.934955, Testing Accuracy: 0.878444 +EPOCH:: 184, Traininig Loss: 0.206980, Testing Loss: 0.548367, Best Loss: 0.422093 +EPOCH:: 184, Traininig Accuracy: 0.931698, Testing Accuracy: 0.878039 +EPOCH:: 185, Traininig Loss: 0.213269, Testing Loss: 0.487385, Best Loss: 0.422093 +EPOCH:: 185, Traininig Accuracy: 0.929764, Testing Accuracy: 0.882091 +EPOCH:: 186, Traininig Loss: 0.203704, Testing Loss: 0.526038, Best Loss: 0.422093 +EPOCH:: 186, Traininig Accuracy: 0.934548, Testing Accuracy: 0.876823 +EPOCH:: 187, Traininig Loss: 0.204246, Testing Loss: 0.504255, Best Loss: 0.422093 +EPOCH:: 187, Traininig Accuracy: 0.932410, Testing Accuracy: 0.876418 +EPOCH:: 188, Traininig Loss: 0.198678, Testing Loss: 0.554111, Best Loss: 0.422093 +EPOCH:: 188, Traininig Accuracy: 0.932410, Testing Accuracy: 0.876013 +EPOCH:: 189, Traininig Loss: 0.193062, Testing Loss: 0.565770, Best Loss: 0.422093 +EPOCH:: 189, Traininig Accuracy: 0.933734, Testing Accuracy: 0.876823 +EPOCH:: 190, Traininig Loss: 0.190854, Testing Loss: 0.508650, Best Loss: 0.422093 +EPOCH:: 190, Traininig Accuracy: 0.932919, Testing Accuracy: 0.885737 +EPOCH:: 191, Traininig Loss: 0.200551, Testing Loss: 0.564246, Best Loss: 0.422093 +EPOCH:: 191, Traininig Accuracy: 0.932614, Testing Accuracy: 0.867099 +EPOCH:: 192, Traininig Loss: 0.195182, Testing Loss: 0.534619, Best Loss: 0.422093 +EPOCH:: 192, Traininig Accuracy: 0.932919, Testing Accuracy: 0.880065 +EPOCH:: 193, Traininig Loss: 0.197477, Testing Loss: 0.558921, Best Loss: 0.422093 +EPOCH:: 193, Traininig Accuracy: 0.935362, Testing Accuracy: 0.873987 +EPOCH:: 194, Traininig Loss: 0.186354, Testing Loss: 0.544411, Best Loss: 0.422093 +EPOCH:: 194, Traininig Accuracy: 0.936686, Testing Accuracy: 0.876418 +EPOCH:: 195, Traininig Loss: 0.205543, Testing Loss: 0.566262, Best Loss: 0.422093 +EPOCH:: 195, Traininig Accuracy: 0.930884, Testing Accuracy: 0.869125 +EPOCH:: 196, Traininig Loss: 0.186250, Testing Loss: 0.567027, Best Loss: 0.422093 +EPOCH:: 196, Traininig Accuracy: 0.940452, Testing Accuracy: 0.880470 +EPOCH:: 197, Traininig Loss: 0.193534, Testing Loss: 0.542523, Best Loss: 0.422093 +EPOCH:: 197, Traininig Accuracy: 0.934141, Testing Accuracy: 0.879660 +EPOCH:: 198, Traininig Loss: 0.198960, Testing Loss: 0.516693, Best Loss: 0.422093 +EPOCH:: 198, Traininig Accuracy: 0.933937, Testing Accuracy: 0.879660 +EPOCH:: 199, Traininig Loss: 0.183889, Testing Loss: 0.493874, Best Loss: 0.422093 +EPOCH:: 199, Traininig Accuracy: 0.940045, Testing Accuracy: 0.882496 +EPOCH:: 200, Traininig Loss: 0.199322, Testing Loss: 0.548099, Best Loss: 0.422093 +EPOCH:: 200, Traininig Accuracy: 0.932818, Testing Accuracy: 0.871961 diff --git a/thirdparty/learning3d/pretrained/exp_curvenet/models/model.t7 b/thirdparty/learning3d/pretrained/exp_curvenet/models/model.t7 new file mode 100644 index 0000000000000000000000000000000000000000..61005d90d54395d9e8f57c40fdf44a1221806a8d --- /dev/null +++ b/thirdparty/learning3d/pretrained/exp_curvenet/models/model.t7 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ccdb4c76e1279403845c1ff4be44107aa2d862574517990820759f463eddf55b +size 8684298 diff --git a/thirdparty/learning3d/pretrained/exp_curvenet/run.log b/thirdparty/learning3d/pretrained/exp_curvenet/run.log new file mode 100644 index 0000000000000000000000000000000000000000..d992023d7ba35dec63c9384a4f1407590fa09adf --- /dev/null +++ b/thirdparty/learning3d/pretrained/exp_curvenet/run.log @@ -0,0 +1,402 @@ +Namespace(batch_size=32, dataset='modelnet40', dropout=0.5, emb_dims=1024, epochs=200, eval=False, exp_name='curvenet_cls', k=20, lr=0.001, model='curvenet_cls', model_path='', momentum=0.9, no_cuda=False, num_points=1024, scheduler='cos', seed=1, test_batch_size=16, use_sgd=True) +Using GPU : 0 from 1 devices +Train 0, loss: 2.553381, train acc: 0.519442, train avg acc: 0.363914 +Test 0, loss: 2.284505, test acc: 0.611831, test avg acc: 0.517733 +Train 1, loss: 2.185607, train acc: 0.657268, train avg acc: 0.527235 +Test 1, loss: 1.940870, test acc: 0.726904, test avg acc: 0.651843 +Train 2, loss: 2.008967, train acc: 0.723432, train avg acc: 0.599584 +Test 2, loss: 1.879364, test acc: 0.752836, test avg acc: 0.693610 +Train 3, loss: 1.906084, train acc: 0.764353, train avg acc: 0.659032 +Test 3, loss: 1.748453, test acc: 0.816451, test avg acc: 0.765576 +Train 4, loss: 1.851210, train acc: 0.785627, train avg acc: 0.687008 +Test 4, loss: 1.726572, test acc: 0.833063, test avg acc: 0.764762 +Train 5, loss: 1.819152, train acc: 0.798860, train avg acc: 0.702477 +Test 5, loss: 1.721637, test acc: 0.838736, test avg acc: 0.772209 +Train 6, loss: 1.772816, train acc: 0.817284, train avg acc: 0.726595 +Test 6, loss: 1.685837, test acc: 0.850486, test avg acc: 0.772616 +Train 7, loss: 1.751429, train acc: 0.826954, train avg acc: 0.741957 +Test 7, loss: 1.665793, test acc: 0.855348, test avg acc: 0.791884 +Train 8, loss: 1.740417, train acc: 0.830008, train avg acc: 0.746824 +Test 8, loss: 1.660230, test acc: 0.852917, test avg acc: 0.783762 +Train 9, loss: 1.720534, train acc: 0.841307, train avg acc: 0.762723 +Test 9, loss: 1.622786, test acc: 0.871151, test avg acc: 0.815971 +Train 10, loss: 1.698032, train acc: 0.847313, train avg acc: 0.771484 +Test 10, loss: 1.651796, test acc: 0.856969, test avg acc: 0.812669 +Train 11, loss: 1.687919, train acc: 0.853624, train avg acc: 0.781313 +Test 11, loss: 1.616611, test acc: 0.874797, test avg acc: 0.820087 +Train 12, loss: 1.673256, train acc: 0.861156, train avg acc: 0.794086 +Test 12, loss: 1.616116, test acc: 0.884117, test avg acc: 0.846628 +Train 13, loss: 1.666446, train acc: 0.860342, train avg acc: 0.784649 +Test 13, loss: 1.592031, test acc: 0.884117, test avg acc: 0.815703 +Train 14, loss: 1.650977, train acc: 0.863192, train avg acc: 0.794394 +Test 14, loss: 1.575398, test acc: 0.882091, test avg acc: 0.831029 +Train 15, loss: 1.652159, train acc: 0.867264, train avg acc: 0.801837 +Test 15, loss: 1.575664, test acc: 0.894246, test avg acc: 0.837919 +Train 16, loss: 1.637970, train acc: 0.873982, train avg acc: 0.809402 +Test 16, loss: 1.586452, test acc: 0.895867, test avg acc: 0.861500 +Train 17, loss: 1.629779, train acc: 0.873677, train avg acc: 0.808300 +Test 17, loss: 1.579189, test acc: 0.897488, test avg acc: 0.842494 +Train 18, loss: 1.615616, train acc: 0.882024, train avg acc: 0.823750 +Test 18, loss: 1.572695, test acc: 0.890194, test avg acc: 0.849163 +Train 19, loss: 1.607200, train acc: 0.885383, train avg acc: 0.826906 +Test 19, loss: 1.582655, test acc: 0.888169, test avg acc: 0.847238 +Train 20, loss: 1.610623, train acc: 0.883550, train avg acc: 0.824119 +Test 20, loss: 1.569853, test acc: 0.888979, test avg acc: 0.847105 +Train 21, loss: 1.606241, train acc: 0.884874, train avg acc: 0.823632 +Test 21, loss: 1.554440, test acc: 0.892626, test avg acc: 0.843506 +Train 22, loss: 1.604799, train acc: 0.886808, train avg acc: 0.828303 +Test 22, loss: 1.558887, test acc: 0.896272, test avg acc: 0.848273 +Train 23, loss: 1.594975, train acc: 0.891694, train avg acc: 0.838561 +Test 23, loss: 1.550027, test acc: 0.886143, test avg acc: 0.840919 +Train 24, loss: 1.593072, train acc: 0.889454, train avg acc: 0.833797 +Test 24, loss: 1.547546, test acc: 0.898703, test avg acc: 0.850453 +Train 25, loss: 1.592162, train acc: 0.887622, train avg acc: 0.828789 +Test 25, loss: 1.590366, test acc: 0.882091, test avg acc: 0.833814 +Train 26, loss: 1.586758, train acc: 0.891897, train avg acc: 0.836839 +Test 26, loss: 1.581689, test acc: 0.895462, test avg acc: 0.847006 +Train 27, loss: 1.578960, train acc: 0.897598, train avg acc: 0.844414 +Test 27, loss: 1.533518, test acc: 0.909643, test avg acc: 0.862384 +Train 28, loss: 1.573108, train acc: 0.898921, train avg acc: 0.846831 +Test 28, loss: 1.565433, test acc: 0.895462, test avg acc: 0.858744 +Train 29, loss: 1.572497, train acc: 0.896376, train avg acc: 0.842791 +Test 29, loss: 1.543194, test acc: 0.890600, test avg acc: 0.856884 +Train 30, loss: 1.570240, train acc: 0.899226, train avg acc: 0.847935 +Test 30, loss: 1.529029, test acc: 0.899514, test avg acc: 0.861215 +Train 31, loss: 1.564603, train acc: 0.901873, train avg acc: 0.849873 +Test 31, loss: 1.536715, test acc: 0.896677, test avg acc: 0.849430 +Train 32, loss: 1.565059, train acc: 0.900346, train avg acc: 0.848004 +Test 32, loss: 1.546312, test acc: 0.889789, test avg acc: 0.853134 +Train 33, loss: 1.561226, train acc: 0.901771, train avg acc: 0.849670 +Test 33, loss: 1.540174, test acc: 0.909643, test avg acc: 0.873064 +Train 34, loss: 1.556484, train acc: 0.904520, train avg acc: 0.854001 +Test 34, loss: 1.524601, test acc: 0.895867, test avg acc: 0.848680 +Train 35, loss: 1.556314, train acc: 0.902077, train avg acc: 0.849068 +Test 35, loss: 1.540999, test acc: 0.895057, test avg acc: 0.868802 +Train 36, loss: 1.551909, train acc: 0.907675, train avg acc: 0.858566 +Test 36, loss: 1.540717, test acc: 0.889789, test avg acc: 0.854331 +Train 37, loss: 1.548690, train acc: 0.906861, train avg acc: 0.859892 +Test 37, loss: 1.524987, test acc: 0.896677, test avg acc: 0.855721 +Train 38, loss: 1.546811, train acc: 0.911645, train avg acc: 0.865765 +Test 38, loss: 1.511287, test acc: 0.910859, test avg acc: 0.864802 +Train 39, loss: 1.540677, train acc: 0.910525, train avg acc: 0.864029 +Test 39, loss: 1.534779, test acc: 0.902755, test avg acc: 0.873727 +Train 40, loss: 1.544376, train acc: 0.906759, train avg acc: 0.856469 +Test 40, loss: 1.517901, test acc: 0.901135, test avg acc: 0.853506 +Train 41, loss: 1.538360, train acc: 0.912663, train avg acc: 0.866992 +Test 41, loss: 1.518002, test acc: 0.912885, test avg acc: 0.873535 +Train 42, loss: 1.540289, train acc: 0.911747, train avg acc: 0.862972 +Test 42, loss: 1.527437, test acc: 0.906402, test avg acc: 0.870209 +Train 43, loss: 1.531351, train acc: 0.916226, train avg acc: 0.872401 +Test 43, loss: 1.513147, test acc: 0.920583, test avg acc: 0.890994 +Train 44, loss: 1.532312, train acc: 0.914495, train avg acc: 0.872002 +Test 44, loss: 1.526433, test acc: 0.901135, test avg acc: 0.869994 +Train 45, loss: 1.534571, train acc: 0.910932, train avg acc: 0.864636 +Test 45, loss: 1.534035, test acc: 0.886143, test avg acc: 0.836262 +Train 46, loss: 1.528407, train acc: 0.915004, train avg acc: 0.868683 +Test 46, loss: 1.506410, test acc: 0.917342, test avg acc: 0.881878 +Train 47, loss: 1.526619, train acc: 0.917651, train avg acc: 0.871924 +Test 47, loss: 1.539452, test acc: 0.907212, test avg acc: 0.871837 +Train 48, loss: 1.525561, train acc: 0.916022, train avg acc: 0.873072 +Test 48, loss: 1.519687, test acc: 0.902350, test avg acc: 0.868884 +Train 49, loss: 1.521203, train acc: 0.919992, train avg acc: 0.882414 +Test 49, loss: 1.552516, test acc: 0.896677, test avg acc: 0.873878 +Train 50, loss: 1.518707, train acc: 0.921112, train avg acc: 0.879215 +Test 50, loss: 1.510973, test acc: 0.908428, test avg acc: 0.871302 +Train 51, loss: 1.516677, train acc: 0.918058, train avg acc: 0.872589 +Test 51, loss: 1.498656, test acc: 0.914911, test avg acc: 0.869203 +Train 52, loss: 1.515393, train acc: 0.920501, train avg acc: 0.880639 +Test 52, loss: 1.506762, test acc: 0.913290, test avg acc: 0.885959 +Train 53, loss: 1.511586, train acc: 0.922129, train avg acc: 0.882549 +Test 53, loss: 1.517351, test acc: 0.902755, test avg acc: 0.867547 +Train 54, loss: 1.506719, train acc: 0.924471, train avg acc: 0.884644 +Test 54, loss: 1.513396, test acc: 0.910859, test avg acc: 0.888576 +Train 55, loss: 1.510641, train acc: 0.923147, train avg acc: 0.883542 +Test 55, loss: 1.502226, test acc: 0.909643, test avg acc: 0.871459 +Train 56, loss: 1.504099, train acc: 0.925285, train avg acc: 0.883887 +Test 56, loss: 1.538792, test acc: 0.899919, test avg acc: 0.857581 +Train 57, loss: 1.505667, train acc: 0.919992, train avg acc: 0.877367 +Test 57, loss: 1.528123, test acc: 0.910454, test avg acc: 0.886535 +Train 58, loss: 1.505072, train acc: 0.922537, train avg acc: 0.880666 +Test 58, loss: 1.503437, test acc: 0.915316, test avg acc: 0.870547 +Train 59, loss: 1.500102, train acc: 0.925590, train avg acc: 0.886281 +Test 59, loss: 1.510341, test acc: 0.906807, test avg acc: 0.868395 +Train 60, loss: 1.499958, train acc: 0.925489, train avg acc: 0.887271 +Test 60, loss: 1.513796, test acc: 0.901540, test avg acc: 0.868047 +Train 61, loss: 1.500781, train acc: 0.924572, train avg acc: 0.886197 +Test 61, loss: 1.500869, test acc: 0.906807, test avg acc: 0.879512 +Train 62, loss: 1.498860, train acc: 0.925794, train avg acc: 0.887131 +Test 62, loss: 1.494357, test acc: 0.914506, test avg acc: 0.880622 +Train 63, loss: 1.492394, train acc: 0.928441, train avg acc: 0.891148 +Test 63, loss: 1.527193, test acc: 0.902755, test avg acc: 0.882238 +Train 64, loss: 1.491991, train acc: 0.927728, train avg acc: 0.889142 +Test 64, loss: 1.486377, test acc: 0.921799, test avg acc: 0.881872 +Train 65, loss: 1.492308, train acc: 0.929255, train avg acc: 0.893671 +Test 65, loss: 1.491146, test acc: 0.913290, test avg acc: 0.871279 +Train 66, loss: 1.488734, train acc: 0.928441, train avg acc: 0.895440 +Test 66, loss: 1.494736, test acc: 0.912885, test avg acc: 0.867244 +Train 67, loss: 1.483914, train acc: 0.932614, train avg acc: 0.896769 +Test 67, loss: 1.499969, test acc: 0.916532, test avg acc: 0.881419 +Train 68, loss: 1.477023, train acc: 0.935464, train avg acc: 0.901742 +Test 68, loss: 1.519570, test acc: 0.902350, test avg acc: 0.879331 +Train 69, loss: 1.482966, train acc: 0.932512, train avg acc: 0.897494 +Test 69, loss: 1.498173, test acc: 0.906402, test avg acc: 0.872006 +Train 70, loss: 1.479897, train acc: 0.932309, train avg acc: 0.897190 +Test 70, loss: 1.549330, test acc: 0.888574, test avg acc: 0.874651 +Train 71, loss: 1.480803, train acc: 0.934650, train avg acc: 0.898646 +Test 71, loss: 1.523638, test acc: 0.899919, test avg acc: 0.867529 +Train 72, loss: 1.474618, train acc: 0.937296, train avg acc: 0.907065 +Test 72, loss: 1.490504, test acc: 0.915316, test avg acc: 0.885645 +Train 73, loss: 1.471686, train acc: 0.938518, train avg acc: 0.905596 +Test 73, loss: 1.490388, test acc: 0.916126, test avg acc: 0.887512 +Train 74, loss: 1.467715, train acc: 0.939230, train avg acc: 0.906807 +Test 74, loss: 1.484522, test acc: 0.914506, test avg acc: 0.877622 +Train 75, loss: 1.472212, train acc: 0.936584, train avg acc: 0.905093 +Test 75, loss: 1.477505, test acc: 0.918558, test avg acc: 0.880419 +Train 76, loss: 1.464531, train acc: 0.940147, train avg acc: 0.908744 +Test 76, loss: 1.503772, test acc: 0.911264, test avg acc: 0.869983 +Train 77, loss: 1.468750, train acc: 0.938416, train avg acc: 0.904290 +Test 77, loss: 1.496593, test acc: 0.909643, test avg acc: 0.883081 +Train 78, loss: 1.463796, train acc: 0.938620, train avg acc: 0.904819 +Test 78, loss: 1.478415, test acc: 0.921394, test avg acc: 0.881831 +Train 79, loss: 1.456545, train acc: 0.944015, train avg acc: 0.914987 +Test 79, loss: 1.487149, test acc: 0.916126, test avg acc: 0.884703 +Train 80, loss: 1.455010, train acc: 0.943302, train avg acc: 0.913107 +Test 80, loss: 1.495837, test acc: 0.911264, test avg acc: 0.888872 +Train 81, loss: 1.453500, train acc: 0.943913, train avg acc: 0.913503 +Test 81, loss: 1.478653, test acc: 0.920178, test avg acc: 0.885343 +Train 82, loss: 1.457649, train acc: 0.943913, train avg acc: 0.915342 +Test 82, loss: 1.474857, test acc: 0.925041, test avg acc: 0.895948 +Train 83, loss: 1.458385, train acc: 0.939536, train avg acc: 0.907794 +Test 83, loss: 1.484646, test acc: 0.917342, test avg acc: 0.891000 +Train 84, loss: 1.449037, train acc: 0.946458, train avg acc: 0.917879 +Test 84, loss: 1.495796, test acc: 0.903971, test avg acc: 0.864419 +Train 85, loss: 1.449775, train acc: 0.948188, train avg acc: 0.920456 +Test 85, loss: 1.481370, test acc: 0.916937, test avg acc: 0.881663 +Train 86, loss: 1.447736, train acc: 0.945033, train avg acc: 0.914466 +Test 86, loss: 1.474512, test acc: 0.918963, test avg acc: 0.880488 +Train 87, loss: 1.447462, train acc: 0.945542, train avg acc: 0.917767 +Test 87, loss: 1.484332, test acc: 0.914911, test avg acc: 0.885797 +Train 88, loss: 1.447178, train acc: 0.945949, train avg acc: 0.918788 +Test 88, loss: 1.486459, test acc: 0.910859, test avg acc: 0.872506 +Train 89, loss: 1.442397, train acc: 0.950733, train avg acc: 0.926467 +Test 89, loss: 1.474762, test acc: 0.927877, test avg acc: 0.894331 +Train 90, loss: 1.444485, train acc: 0.947272, train avg acc: 0.919088 +Test 90, loss: 1.478542, test acc: 0.919368, test avg acc: 0.892488 +Train 91, loss: 1.438866, train acc: 0.951445, train avg acc: 0.925236 +Test 91, loss: 1.485061, test acc: 0.916532, test avg acc: 0.879709 +Train 92, loss: 1.435064, train acc: 0.950428, train avg acc: 0.924938 +Test 92, loss: 1.464838, test acc: 0.923825, test avg acc: 0.887744 +Train 93, loss: 1.434353, train acc: 0.953379, train avg acc: 0.929985 +Test 93, loss: 1.494110, test acc: 0.919368, test avg acc: 0.884244 +Train 94, loss: 1.434108, train acc: 0.951751, train avg acc: 0.926881 +Test 94, loss: 1.499822, test acc: 0.903566, test avg acc: 0.880081 +Train 95, loss: 1.433999, train acc: 0.950733, train avg acc: 0.925111 +Test 95, loss: 1.473818, test acc: 0.916532, test avg acc: 0.886826 +Train 96, loss: 1.429019, train acc: 0.952158, train avg acc: 0.924401 +Test 96, loss: 1.481415, test acc: 0.908833, test avg acc: 0.877058 +Train 97, loss: 1.424356, train acc: 0.955008, train avg acc: 0.931854 +Test 97, loss: 1.471671, test acc: 0.914911, test avg acc: 0.874413 +Train 98, loss: 1.423858, train acc: 0.953888, train avg acc: 0.932340 +Test 98, loss: 1.466855, test acc: 0.923825, test avg acc: 0.884576 +Train 99, loss: 1.425324, train acc: 0.955517, train avg acc: 0.932532 +Test 99, loss: 1.468501, test acc: 0.918558, test avg acc: 0.884285 +Train 100, loss: 1.422439, train acc: 0.955721, train avg acc: 0.933695 +Test 100, loss: 1.474414, test acc: 0.921799, test avg acc: 0.890413 +Train 101, loss: 1.416836, train acc: 0.959080, train avg acc: 0.939132 +Test 101, loss: 1.466817, test acc: 0.926661, test avg acc: 0.890948 +Train 102, loss: 1.416004, train acc: 0.958876, train avg acc: 0.937670 +Test 102, loss: 1.461437, test acc: 0.919368, test avg acc: 0.882326 +Train 103, loss: 1.412157, train acc: 0.960708, train avg acc: 0.939308 +Test 103, loss: 1.475800, test acc: 0.915721, test avg acc: 0.891791 +Train 104, loss: 1.416123, train acc: 0.958876, train avg acc: 0.937776 +Test 104, loss: 1.455841, test acc: 0.921394, test avg acc: 0.889791 +Train 105, loss: 1.414805, train acc: 0.957655, train avg acc: 0.936897 +Test 105, loss: 1.462708, test acc: 0.924635, test avg acc: 0.893360 +Train 106, loss: 1.412066, train acc: 0.959080, train avg acc: 0.939651 +Test 106, loss: 1.459283, test acc: 0.921799, test avg acc: 0.885872 +Train 107, loss: 1.408658, train acc: 0.957960, train avg acc: 0.935958 +Test 107, loss: 1.452565, test acc: 0.924635, test avg acc: 0.894698 +Train 108, loss: 1.406476, train acc: 0.959792, train avg acc: 0.938589 +Test 108, loss: 1.470847, test acc: 0.916532, test avg acc: 0.892459 +Train 109, loss: 1.400594, train acc: 0.962134, train avg acc: 0.942994 +Test 109, loss: 1.460697, test acc: 0.922609, test avg acc: 0.884820 +Train 110, loss: 1.401070, train acc: 0.963457, train avg acc: 0.944407 +Test 110, loss: 1.460874, test acc: 0.918963, test avg acc: 0.885541 +Train 111, loss: 1.400745, train acc: 0.962948, train avg acc: 0.943753 +Test 111, loss: 1.457981, test acc: 0.919773, test avg acc: 0.892285 +Train 112, loss: 1.399623, train acc: 0.962948, train avg acc: 0.944629 +Test 112, loss: 1.456726, test acc: 0.920178, test avg acc: 0.889785 +Train 113, loss: 1.400500, train acc: 0.962846, train avg acc: 0.944314 +Test 113, loss: 1.452554, test acc: 0.924635, test avg acc: 0.894872 +Train 114, loss: 1.392361, train acc: 0.966103, train avg acc: 0.948119 +Test 114, loss: 1.453766, test acc: 0.919368, test avg acc: 0.885872 +Train 115, loss: 1.394078, train acc: 0.964984, train avg acc: 0.947381 +Test 115, loss: 1.453156, test acc: 0.927877, test avg acc: 0.897994 +Train 116, loss: 1.389535, train acc: 0.968037, train avg acc: 0.950987 +Test 116, loss: 1.454990, test acc: 0.916126, test avg acc: 0.885576 +Train 117, loss: 1.388315, train acc: 0.969055, train avg acc: 0.954054 +Test 117, loss: 1.450748, test acc: 0.927877, test avg acc: 0.890738 +Train 118, loss: 1.390117, train acc: 0.966612, train avg acc: 0.950918 +Test 118, loss: 1.444256, test acc: 0.927066, test avg acc: 0.895872 +Train 119, loss: 1.384259, train acc: 0.969361, train avg acc: 0.952624 +Test 119, loss: 1.459840, test acc: 0.920178, test avg acc: 0.900529 +Train 120, loss: 1.384487, train acc: 0.969055, train avg acc: 0.952495 +Test 120, loss: 1.452094, test acc: 0.925851, test avg acc: 0.897994 +Train 121, loss: 1.378914, train acc: 0.972618, train avg acc: 0.958017 +Test 121, loss: 1.451919, test acc: 0.923015, test avg acc: 0.891448 +Train 122, loss: 1.380632, train acc: 0.970480, train avg acc: 0.955888 +Test 122, loss: 1.452885, test acc: 0.924635, test avg acc: 0.893453 +Train 123, loss: 1.379335, train acc: 0.971702, train avg acc: 0.955822 +Test 123, loss: 1.455207, test acc: 0.914911, test avg acc: 0.884285 +Train 124, loss: 1.380079, train acc: 0.971498, train avg acc: 0.957804 +Test 124, loss: 1.449668, test acc: 0.927472, test avg acc: 0.895744 +Train 125, loss: 1.375201, train acc: 0.973840, train avg acc: 0.959704 +Test 125, loss: 1.442129, test acc: 0.927472, test avg acc: 0.895279 +Train 126, loss: 1.373267, train acc: 0.972923, train avg acc: 0.958283 +Test 126, loss: 1.445254, test acc: 0.929498, test avg acc: 0.895488 +Train 127, loss: 1.371576, train acc: 0.973025, train avg acc: 0.962030 +Test 127, loss: 1.452941, test acc: 0.924230, test avg acc: 0.890326 +Train 128, loss: 1.370595, train acc: 0.974959, train avg acc: 0.962607 +Test 128, loss: 1.457340, test acc: 0.921394, test avg acc: 0.888076 +Train 129, loss: 1.367417, train acc: 0.974959, train avg acc: 0.962549 +Test 129, loss: 1.481700, test acc: 0.912480, test avg acc: 0.888988 +Train 130, loss: 1.364362, train acc: 0.976283, train avg acc: 0.964809 +Test 130, loss: 1.450849, test acc: 0.925851, test avg acc: 0.894570 +Train 131, loss: 1.363496, train acc: 0.976690, train avg acc: 0.965029 +Test 131, loss: 1.440953, test acc: 0.930308, test avg acc: 0.901866 +Train 132, loss: 1.359947, train acc: 0.978420, train avg acc: 0.967701 +Test 132, loss: 1.443655, test acc: 0.927877, test avg acc: 0.902866 +Train 133, loss: 1.359872, train acc: 0.978726, train avg acc: 0.967003 +Test 133, loss: 1.448682, test acc: 0.928282, test avg acc: 0.894116 +Train 134, loss: 1.359942, train acc: 0.979031, train avg acc: 0.969636 +Test 134, loss: 1.437363, test acc: 0.927066, test avg acc: 0.895238 +Train 135, loss: 1.357034, train acc: 0.978013, train avg acc: 0.966050 +Test 135, loss: 1.442055, test acc: 0.925851, test avg acc: 0.898541 +Train 136, loss: 1.356454, train acc: 0.977911, train avg acc: 0.967122 +Test 136, loss: 1.437095, test acc: 0.928282, test avg acc: 0.891163 +Train 137, loss: 1.356462, train acc: 0.978624, train avg acc: 0.967042 +Test 137, loss: 1.445502, test acc: 0.923825, test avg acc: 0.892198 +Train 138, loss: 1.354065, train acc: 0.980660, train avg acc: 0.970762 +Test 138, loss: 1.443782, test acc: 0.924230, test avg acc: 0.892738 +Train 139, loss: 1.349804, train acc: 0.982899, train avg acc: 0.973972 +Test 139, loss: 1.438650, test acc: 0.932739, test avg acc: 0.905698 +Train 140, loss: 1.351906, train acc: 0.980049, train avg acc: 0.970755 +Test 140, loss: 1.444854, test acc: 0.923015, test avg acc: 0.893401 +Train 141, loss: 1.346747, train acc: 0.983408, train avg acc: 0.974533 +Test 141, loss: 1.436907, test acc: 0.928687, test avg acc: 0.904657 +Train 142, loss: 1.348775, train acc: 0.981372, train avg acc: 0.971543 +Test 142, loss: 1.437806, test acc: 0.928687, test avg acc: 0.899372 +Train 143, loss: 1.347446, train acc: 0.981881, train avg acc: 0.971910 +Test 143, loss: 1.439646, test acc: 0.930308, test avg acc: 0.901529 +Train 144, loss: 1.345089, train acc: 0.982695, train avg acc: 0.973840 +Test 144, loss: 1.440166, test acc: 0.932334, test avg acc: 0.896651 +Train 145, loss: 1.341682, train acc: 0.984019, train avg acc: 0.975267 +Test 145, loss: 1.435072, test acc: 0.929498, test avg acc: 0.895657 +Train 146, loss: 1.343034, train acc: 0.983306, train avg acc: 0.975333 +Test 146, loss: 1.429245, test acc: 0.932334, test avg acc: 0.900703 +Train 147, loss: 1.340799, train acc: 0.984324, train avg acc: 0.977472 +Test 147, loss: 1.427680, test acc: 0.933549, test avg acc: 0.909570 +Train 148, loss: 1.339195, train acc: 0.985037, train avg acc: 0.977626 +Test 148, loss: 1.431339, test acc: 0.933144, test avg acc: 0.903779 +Train 149, loss: 1.336964, train acc: 0.981983, train avg acc: 0.973705 +Test 149, loss: 1.438228, test acc: 0.929092, test avg acc: 0.895692 +Train 150, loss: 1.336458, train acc: 0.985342, train avg acc: 0.976717 +Test 150, loss: 1.435326, test acc: 0.930308, test avg acc: 0.901279 +Train 151, loss: 1.334714, train acc: 0.987581, train avg acc: 0.981411 +Test 151, loss: 1.436303, test acc: 0.932334, test avg acc: 0.902610 +Train 152, loss: 1.333098, train acc: 0.986665, train avg acc: 0.979717 +Test 152, loss: 1.435353, test acc: 0.931524, test avg acc: 0.903860 +Train 153, loss: 1.330774, train acc: 0.986055, train avg acc: 0.979166 +Test 153, loss: 1.436693, test acc: 0.927877, test avg acc: 0.901360 +Train 154, loss: 1.332253, train acc: 0.987276, train avg acc: 0.980040 +Test 154, loss: 1.430879, test acc: 0.930308, test avg acc: 0.902738 +Train 155, loss: 1.328320, train acc: 0.989210, train avg acc: 0.982805 +Test 155, loss: 1.436866, test acc: 0.931118, test avg acc: 0.903692 +Train 156, loss: 1.328442, train acc: 0.988905, train avg acc: 0.983116 +Test 156, loss: 1.431555, test acc: 0.929903, test avg acc: 0.901779 +Train 157, loss: 1.327213, train acc: 0.987378, train avg acc: 0.980523 +Test 157, loss: 1.434253, test acc: 0.929903, test avg acc: 0.897110 +Train 158, loss: 1.324947, train acc: 0.988192, train avg acc: 0.982681 +Test 158, loss: 1.432485, test acc: 0.929092, test avg acc: 0.903157 +Train 159, loss: 1.325103, train acc: 0.989108, train avg acc: 0.983978 +Test 159, loss: 1.431041, test acc: 0.931118, test avg acc: 0.903238 +Train 160, loss: 1.323563, train acc: 0.989007, train avg acc: 0.982647 +Test 160, loss: 1.430821, test acc: 0.934765, test avg acc: 0.908698 +Train 161, loss: 1.323121, train acc: 0.989312, train avg acc: 0.983739 +Test 161, loss: 1.441819, test acc: 0.927066, test avg acc: 0.907157 +Train 162, loss: 1.322691, train acc: 0.989007, train avg acc: 0.983063 +Test 162, loss: 1.431337, test acc: 0.929903, test avg acc: 0.900698 +Train 163, loss: 1.319016, train acc: 0.990533, train avg acc: 0.985634 +Test 163, loss: 1.432316, test acc: 0.931118, test avg acc: 0.902244 +Train 164, loss: 1.319565, train acc: 0.991144, train avg acc: 0.986828 +Test 164, loss: 1.430837, test acc: 0.932739, test avg acc: 0.903651 +Train 165, loss: 1.318858, train acc: 0.989312, train avg acc: 0.984123 +Test 165, loss: 1.429921, test acc: 0.933549, test avg acc: 0.907733 +Train 166, loss: 1.318777, train acc: 0.991857, train avg acc: 0.988386 +Test 166, loss: 1.432584, test acc: 0.929498, test avg acc: 0.900779 +Train 167, loss: 1.317654, train acc: 0.991857, train avg acc: 0.988875 +Test 167, loss: 1.430880, test acc: 0.933144, test avg acc: 0.910116 +Train 168, loss: 1.319869, train acc: 0.990432, train avg acc: 0.986232 +Test 168, loss: 1.427063, test acc: 0.930713, test avg acc: 0.901360 +Train 169, loss: 1.315010, train acc: 0.990941, train avg acc: 0.987576 +Test 169, loss: 1.428392, test acc: 0.932739, test avg acc: 0.907448 +Train 170, loss: 1.317478, train acc: 0.989821, train avg acc: 0.984052 +Test 170, loss: 1.430019, test acc: 0.933955, test avg acc: 0.905238 +Train 171, loss: 1.313504, train acc: 0.990737, train avg acc: 0.986832 +Test 171, loss: 1.431467, test acc: 0.929092, test avg acc: 0.899907 +Train 172, loss: 1.313151, train acc: 0.991958, train avg acc: 0.987459 +Test 172, loss: 1.430627, test acc: 0.931929, test avg acc: 0.906616 +Train 173, loss: 1.314448, train acc: 0.992366, train avg acc: 0.988977 +Test 173, loss: 1.427643, test acc: 0.934765, test avg acc: 0.903820 +Train 174, loss: 1.312394, train acc: 0.991755, train avg acc: 0.987276 +Test 174, loss: 1.426035, test acc: 0.937601, test avg acc: 0.909692 +Train 175, loss: 1.310919, train acc: 0.992976, train avg acc: 0.989202 +Test 175, loss: 1.429768, test acc: 0.933955, test avg acc: 0.905605 +Train 176, loss: 1.312709, train acc: 0.989719, train avg acc: 0.984540 +Test 176, loss: 1.426275, test acc: 0.934360, test avg acc: 0.904692 +Train 177, loss: 1.311465, train acc: 0.991348, train avg acc: 0.986551 +Test 177, loss: 1.426634, test acc: 0.932334, test avg acc: 0.907360 +Train 178, loss: 1.309833, train acc: 0.992875, train avg acc: 0.989316 +Test 178, loss: 1.424590, test acc: 0.935170, test avg acc: 0.910110 +Train 179, loss: 1.310258, train acc: 0.992569, train avg acc: 0.989121 +Test 179, loss: 1.424937, test acc: 0.937601, test avg acc: 0.912442 +Train 180, loss: 1.308947, train acc: 0.992264, train avg acc: 0.988018 +Test 180, loss: 1.428859, test acc: 0.933549, test avg acc: 0.909738 +Train 181, loss: 1.308764, train acc: 0.993078, train avg acc: 0.989643 +Test 181, loss: 1.426770, test acc: 0.936791, test avg acc: 0.912360 +Train 182, loss: 1.307551, train acc: 0.993689, train avg acc: 0.990697 +Test 182, loss: 1.426205, test acc: 0.933144, test avg acc: 0.905820 +Train 183, loss: 1.307422, train acc: 0.992976, train avg acc: 0.989260 +Test 183, loss: 1.425178, test acc: 0.934360, test avg acc: 0.908610 +Train 184, loss: 1.307873, train acc: 0.992976, train avg acc: 0.989615 +Test 184, loss: 1.425414, test acc: 0.936386, test avg acc: 0.909860 +Train 185, loss: 1.307086, train acc: 0.994401, train avg acc: 0.991288 +Test 185, loss: 1.425438, test acc: 0.935981, test avg acc: 0.910610 +Train 186, loss: 1.306580, train acc: 0.994605, train avg acc: 0.991875 +Test 186, loss: 1.424439, test acc: 0.934765, test avg acc: 0.905738 +Train 187, loss: 1.307236, train acc: 0.993282, train avg acc: 0.990312 +Test 187, loss: 1.423681, test acc: 0.935170, test avg acc: 0.905151 +Train 188, loss: 1.305860, train acc: 0.994401, train avg acc: 0.991438 +Test 188, loss: 1.427231, test acc: 0.933549, test avg acc: 0.903151 +Train 189, loss: 1.305664, train acc: 0.994401, train avg acc: 0.991963 +Test 189, loss: 1.427065, test acc: 0.934360, test avg acc: 0.905570 +Train 190, loss: 1.306299, train acc: 0.993893, train avg acc: 0.990945 +Test 190, loss: 1.423927, test acc: 0.936386, test avg acc: 0.908942 +Train 191, loss: 1.305488, train acc: 0.993485, train avg acc: 0.989581 +Test 191, loss: 1.425582, test acc: 0.935981, test avg acc: 0.906901 +Train 192, loss: 1.304498, train acc: 0.994809, train avg acc: 0.991991 +Test 192, loss: 1.427238, test acc: 0.933144, test avg acc: 0.904983 +Train 193, loss: 1.305166, train acc: 0.993282, train avg acc: 0.990853 +Test 193, loss: 1.423776, test acc: 0.938412, test avg acc: 0.911401 +Train 194, loss: 1.304408, train acc: 0.994300, train avg acc: 0.990957 +Test 194, loss: 1.427524, test acc: 0.934360, test avg acc: 0.907692 +Train 195, loss: 1.305089, train acc: 0.994096, train avg acc: 0.991044 +Test 195, loss: 1.426729, test acc: 0.935981, test avg acc: 0.910488 +Train 196, loss: 1.303238, train acc: 0.995012, train avg acc: 0.992255 +Test 196, loss: 1.425034, test acc: 0.938006, test avg acc: 0.913860 +Train 197, loss: 1.304111, train acc: 0.994198, train avg acc: 0.990365 +Test 197, loss: 1.426431, test acc: 0.934360, test avg acc: 0.907529 +Train 198, loss: 1.303994, train acc: 0.994503, train avg acc: 0.992581 +Test 198, loss: 1.424388, test acc: 0.936791, test avg acc: 0.908151 +Train 199, loss: 1.303686, train acc: 0.995012, train avg acc: 0.992411 +Test 199, loss: 1.426077, test acc: 0.933955, test avg acc: 0.909279 diff --git a/thirdparty/learning3d/pretrained/exp_dcp/models/best_model.t7 b/thirdparty/learning3d/pretrained/exp_dcp/models/best_model.t7 new file mode 100644 index 0000000000000000000000000000000000000000..0fc8800cae7b34723e31ca5025633b06c561c8d2 --- /dev/null +++ b/thirdparty/learning3d/pretrained/exp_dcp/models/best_model.t7 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7bdd6aa9dc8a7ddc11c18ddf85a51bc551b7e310e110c08cf911a7c85a6bc922 +size 22365557 diff --git a/thirdparty/learning3d/pretrained/exp_deepgmr/models/best_model.pth b/thirdparty/learning3d/pretrained/exp_deepgmr/models/best_model.pth new file mode 100644 index 0000000000000000000000000000000000000000..7e48807d0c924ca07084d64d50f1021bcb8a08e2 --- /dev/null +++ b/thirdparty/learning3d/pretrained/exp_deepgmr/models/best_model.pth @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:58a8caa576050505a64dbc831345638279cb51ca87498de20fb83a7a434ca131 +size 6137109 diff --git a/thirdparty/learning3d/pretrained/exp_flownet/models/model.best.t7 b/thirdparty/learning3d/pretrained/exp_flownet/models/model.best.t7 new file mode 100644 index 0000000000000000000000000000000000000000..542415ac1804e35401e02b359abb64a842a87332 --- /dev/null +++ b/thirdparty/learning3d/pretrained/exp_flownet/models/model.best.t7 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e0d6ee99caffa00ce9cc91a5c17f9e149dded1f10154cbef9b97e4d2d239db6 +size 4992315 diff --git a/thirdparty/learning3d/pretrained/exp_flownet/run.log b/thirdparty/learning3d/pretrained/exp_flownet/run.log new file mode 100644 index 0000000000000000000000000000000000000000..2f1160879bf738c224dc079f9f44e7f1f00be8dd --- /dev/null +++ b/thirdparty/learning3d/pretrained/exp_flownet/run.log @@ -0,0 +1,788 @@ +Namespace(batch_size=16, cycle=False, dataset='SceneflowDataset', dataset_path='data_processed_maxcut_35_20k_2k_8192', dropout=0.5, emb_dims=512, epochs=250, eval=False, exp_name='exp', gaussian_noise=False, lr=0.001, model='flownet', model_path='', momentum=0.9, no_cuda=False, num_points=2048, seed=1234, test_batch_size=10, unseen=False, use_sgd=True) +==epoch: 0== +mean train EPE loss: 0.431267 +mean test EPE loss: 0.340582 +best test loss till now: 0.340582 +==epoch: 1== +mean train EPE loss: 0.281639 +mean test EPE loss: 0.225758 +best test loss till now: 0.225758 +==epoch: 2== +mean train EPE loss: 0.199420 +mean test EPE loss: 0.486167 +==epoch: 3== +mean train EPE loss: 0.183233 +mean test EPE loss: 0.252737 +==epoch: 4== +mean train EPE loss: 0.152588 +mean test EPE loss: 0.090534 +best test loss till now: 0.090534 +==epoch: 5== +mean train EPE loss: 0.115606 +mean test EPE loss: 0.092835 +==epoch: 6== +mean train EPE loss: 0.104571 +mean test EPE loss: 0.084480 +best test loss till now: 0.084480 +==epoch: 7== +mean train EPE loss: 0.105153 +mean test EPE loss: 0.164466 +==epoch: 8== +mean train EPE loss: 0.108821 +mean test EPE loss: 0.075580 +best test loss till now: 0.075580 +==epoch: 9== +mean train EPE loss: 0.092925 +mean test EPE loss: 0.072869 +best test loss till now: 0.072869 +==epoch: 10== +mean train EPE loss: 0.111956 +mean test EPE loss: 0.074745 +==epoch: 11== +mean train EPE loss: 0.099146 +mean test EPE loss: 0.137278 +==epoch: 12== +mean train EPE loss: 0.094207 +mean test EPE loss: 0.060559 +best test loss till now: 0.060559 +==epoch: 13== +mean train EPE loss: 0.093185 +mean test EPE loss: 0.286756 +==epoch: 14== +mean train EPE loss: 0.119268 +mean test EPE loss: 0.077205 +==epoch: 15== +mean train EPE loss: 0.093827 +mean test EPE loss: 0.086382 +==epoch: 16== +mean train EPE loss: 0.098988 +mean test EPE loss: 0.217489 +==epoch: 17== +mean train EPE loss: 0.097835 +mean test EPE loss: 0.210513 +==epoch: 18== +mean train EPE loss: 0.089085 +mean test EPE loss: 0.086404 +==epoch: 19== +mean train EPE loss: 0.083784 +mean test EPE loss: 0.138668 +==epoch: 20== +mean train EPE loss: 0.082815 +mean test EPE loss: 0.080228 +==epoch: 21== +mean train EPE loss: 0.116256 +mean test EPE loss: 0.129814 +==epoch: 22== +mean train EPE loss: 0.091469 +mean test EPE loss: 0.102912 +==epoch: 23== +mean train EPE loss: 0.082279 +mean test EPE loss: 0.172294 +==epoch: 24== +mean train EPE loss: 0.085325 +mean test EPE loss: 0.161060 +==epoch: 25== +mean train EPE loss: 0.121022 +mean test EPE loss: 0.079917 +==epoch: 26== +mean train EPE loss: 0.091292 +mean test EPE loss: 0.115361 +==epoch: 27== +mean train EPE loss: 0.097489 +mean test EPE loss: 0.220803 +==epoch: 28== +mean train EPE loss: 0.180581 +mean test EPE loss: 0.157473 +==epoch: 29== +mean train EPE loss: 0.138735 +mean test EPE loss: 0.134292 +==epoch: 30== +mean train EPE loss: 0.100302 +mean test EPE loss: 0.084820 +==epoch: 31== +mean train EPE loss: 0.103264 +mean test EPE loss: 0.082913 +==epoch: 32== +mean train EPE loss: 0.121330 +mean test EPE loss: 0.134099 +==epoch: 33== +mean train EPE loss: 0.112699 +mean test EPE loss: 0.311156 +==epoch: 34== +mean train EPE loss: 0.112178 +mean test EPE loss: 0.142419 +==epoch: 35== +mean train EPE loss: 0.095854 +mean test EPE loss: 0.115068 +==epoch: 36== +mean train EPE loss: 0.106807 +mean test EPE loss: 0.137521 +==epoch: 37== +mean train EPE loss: 0.093730 +mean test EPE loss: 0.118231 +==epoch: 38== +mean train EPE loss: 0.093971 +mean test EPE loss: 0.115105 +==epoch: 39== +mean train EPE loss: 0.111744 +mean test EPE loss: 0.116915 +==epoch: 40== +mean train EPE loss: 0.102366 +mean test EPE loss: 0.196135 +==epoch: 41== +mean train EPE loss: 0.128530 +mean test EPE loss: 0.114343 +==epoch: 42== +mean train EPE loss: 0.100045 +mean test EPE loss: 0.119588 +==epoch: 43== +mean train EPE loss: 0.104259 +mean test EPE loss: 0.120017 +==epoch: 44== +mean train EPE loss: 0.094137 +mean test EPE loss: 0.080023 +==epoch: 45== +mean train EPE loss: 0.097274 +mean test EPE loss: 0.524483 +==epoch: 46== +mean train EPE loss: 0.096686 +mean test EPE loss: 0.151619 +==epoch: 47== +mean train EPE loss: 0.098596 +mean test EPE loss: 0.106602 +==epoch: 48== +mean train EPE loss: 0.122597 +mean test EPE loss: 0.116297 +==epoch: 49== +mean train EPE loss: 0.121335 +mean test EPE loss: 0.137051 +==epoch: 50== +mean train EPE loss: 0.110066 +mean test EPE loss: 0.120013 +==epoch: 51== +mean train EPE loss: 0.094533 +mean test EPE loss: 0.074296 +==epoch: 52== +mean train EPE loss: 0.091124 +mean test EPE loss: 0.170632 +==epoch: 53== +mean train EPE loss: 0.083498 +mean test EPE loss: 0.081407 +==epoch: 54== +mean train EPE loss: 0.081401 +mean test EPE loss: 0.122202 +==epoch: 55== +mean train EPE loss: 0.101494 +mean test EPE loss: 0.150662 +==epoch: 56== +mean train EPE loss: 0.107318 +mean test EPE loss: 0.128086 +==epoch: 57== +mean train EPE loss: 0.103620 +mean test EPE loss: 0.534634 +==epoch: 58== +mean train EPE loss: 0.113620 +mean test EPE loss: 0.258004 +==epoch: 59== +mean train EPE loss: 0.094324 +mean test EPE loss: 0.200595 +==epoch: 60== +mean train EPE loss: 0.088799 +mean test EPE loss: 0.091650 +==epoch: 61== +mean train EPE loss: 0.111435 +mean test EPE loss: 0.150889 +==epoch: 62== +mean train EPE loss: 0.095880 +mean test EPE loss: 0.099913 +==epoch: 63== +mean train EPE loss: 0.085993 +mean test EPE loss: 0.096410 +==epoch: 64== +mean train EPE loss: 0.139014 +mean test EPE loss: 0.152282 +==epoch: 65== +mean train EPE loss: 0.120000 +mean test EPE loss: 0.191974 +==epoch: 66== +mean train EPE loss: 0.098411 +mean test EPE loss: 0.077673 +==epoch: 67== +mean train EPE loss: 0.093562 +mean test EPE loss: 0.299697 +==epoch: 68== +mean train EPE loss: 0.092936 +mean test EPE loss: 0.187221 +==epoch: 69== +mean train EPE loss: 0.097947 +mean test EPE loss: 0.089446 +==epoch: 70== +mean train EPE loss: 0.094262 +mean test EPE loss: 0.086140 +==epoch: 71== +mean train EPE loss: 0.087693 +mean test EPE loss: 0.119610 +==epoch: 72== +mean train EPE loss: 0.109871 +mean test EPE loss: 0.125179 +==epoch: 73== +mean train EPE loss: 0.109439 +mean test EPE loss: 0.203852 +==epoch: 74== +mean train EPE loss: 0.061579 +mean test EPE loss: 0.050836 +best test loss till now: 0.050836 +==epoch: 75== +mean train EPE loss: 0.055292 +mean test EPE loss: 0.049345 +best test loss till now: 0.049345 +==epoch: 76== +mean train EPE loss: 0.052402 +mean test EPE loss: 0.049112 +best test loss till now: 0.049112 +==epoch: 77== +mean train EPE loss: 0.051441 +mean test EPE loss: 0.048259 +best test loss till now: 0.048259 +==epoch: 78== +mean train EPE loss: 0.049255 +mean test EPE loss: 0.045184 +best test loss till now: 0.045184 +==epoch: 79== +mean train EPE loss: 0.048349 +mean test EPE loss: 0.044542 +best test loss till now: 0.044542 +==epoch: 80== +mean train EPE loss: 0.047860 +mean test EPE loss: 0.044045 +best test loss till now: 0.044045 +==epoch: 81== +mean train EPE loss: 0.047181 +mean test EPE loss: 0.043438 +best test loss till now: 0.043438 +==epoch: 82== +mean train EPE loss: 0.046353 +mean test EPE loss: 0.043515 +==epoch: 83== +mean train EPE loss: 0.044734 +mean test EPE loss: 0.047540 +==epoch: 84== +mean train EPE loss: 0.044939 +mean test EPE loss: 0.051199 +==epoch: 85== +mean train EPE loss: 0.045149 +mean test EPE loss: 0.047455 +==epoch: 86== +mean train EPE loss: 0.044522 +mean test EPE loss: 0.043278 +best test loss till now: 0.043278 +==epoch: 87== +mean train EPE loss: 0.043332 +mean test EPE loss: 0.041886 +best test loss till now: 0.041886 +==epoch: 88== +mean train EPE loss: 0.042941 +mean test EPE loss: 0.040249 +best test loss till now: 0.040249 +==epoch: 89== +mean train EPE loss: 0.043220 +mean test EPE loss: 0.043912 +==epoch: 90== +mean train EPE loss: 0.042146 +mean test EPE loss: 0.040242 +best test loss till now: 0.040242 +==epoch: 91== +mean train EPE loss: 0.042009 +mean test EPE loss: 0.043131 +==epoch: 92== +mean train EPE loss: 0.044422 +mean test EPE loss: 0.040763 +==epoch: 93== +mean train EPE loss: 0.041697 +mean test EPE loss: 0.040677 +==epoch: 94== +mean train EPE loss: 0.042747 +mean test EPE loss: 0.043065 +==epoch: 95== +mean train EPE loss: 0.041275 +mean test EPE loss: 0.061719 +==epoch: 96== +mean train EPE loss: 0.040938 +mean test EPE loss: 0.040100 +best test loss till now: 0.040100 +==epoch: 97== +mean train EPE loss: 0.040176 +mean test EPE loss: 0.041430 +==epoch: 98== +mean train EPE loss: 0.040095 +mean test EPE loss: 0.043294 +==epoch: 99== +mean train EPE loss: 0.040749 +mean test EPE loss: 0.041395 +==epoch: 100== +mean train EPE loss: 0.039777 +mean test EPE loss: 0.047169 +==epoch: 101== +mean train EPE loss: 0.039507 +mean test EPE loss: 0.042961 +==epoch: 102== +mean train EPE loss: 0.038821 +mean test EPE loss: 0.040169 +==epoch: 103== +mean train EPE loss: 0.038797 +mean test EPE loss: 0.043976 +==epoch: 104== +mean train EPE loss: 0.039189 +mean test EPE loss: 0.041950 +==epoch: 105== +mean train EPE loss: 0.038747 +mean test EPE loss: 0.049623 +==epoch: 106== +mean train EPE loss: 0.038075 +mean test EPE loss: 0.042769 +==epoch: 107== +mean train EPE loss: 0.054479 +mean test EPE loss: 0.042971 +==epoch: 108== +mean train EPE loss: 0.039962 +mean test EPE loss: 0.042330 +==epoch: 109== +mean train EPE loss: 0.038406 +mean test EPE loss: 0.040655 +==epoch: 110== +mean train EPE loss: 0.038010 +mean test EPE loss: 0.043136 +==epoch: 111== +mean train EPE loss: 0.038265 +mean test EPE loss: 0.047118 +==epoch: 112== +mean train EPE loss: 0.038916 +mean test EPE loss: 0.040351 +==epoch: 113== +mean train EPE loss: 0.038712 +mean test EPE loss: 0.042451 +==epoch: 114== +mean train EPE loss: 0.038139 +mean test EPE loss: 0.040773 +==epoch: 115== +mean train EPE loss: 0.036875 +mean test EPE loss: 0.036831 +best test loss till now: 0.036831 +==epoch: 116== +mean train EPE loss: 0.037052 +mean test EPE loss: 0.048804 +==epoch: 117== +mean train EPE loss: 0.044620 +mean test EPE loss: 0.038921 +==epoch: 118== +mean train EPE loss: 0.039674 +mean test EPE loss: 0.041633 +==epoch: 119== +mean train EPE loss: 0.037422 +mean test EPE loss: 0.040810 +==epoch: 120== +mean train EPE loss: 0.039921 +mean test EPE loss: 0.038312 +==epoch: 121== +mean train EPE loss: 0.037769 +mean test EPE loss: 0.043904 +==epoch: 122== +mean train EPE loss: 0.037357 +mean test EPE loss: 0.048388 +==epoch: 123== +mean train EPE loss: 0.036850 +mean test EPE loss: 0.045566 +==epoch: 124== +mean train EPE loss: 0.060290 +mean test EPE loss: 0.053383 +==epoch: 125== +mean train EPE loss: 0.043697 +mean test EPE loss: 0.047074 +==epoch: 126== +mean train EPE loss: 0.038173 +mean test EPE loss: 0.053482 +==epoch: 127== +mean train EPE loss: 0.037987 +mean test EPE loss: 0.038928 +==epoch: 128== +mean train EPE loss: 0.037219 +mean test EPE loss: 0.048666 +==epoch: 129== +mean train EPE loss: 0.039154 +mean test EPE loss: 0.048725 +==epoch: 130== +mean train EPE loss: 0.039520 +mean test EPE loss: 0.646286 +==epoch: 131== +mean train EPE loss: 0.125637 +mean test EPE loss: 0.064231 +==epoch: 132== +mean train EPE loss: 0.068421 +mean test EPE loss: 0.066377 +==epoch: 133== +mean train EPE loss: 0.057878 +mean test EPE loss: 0.059458 +==epoch: 134== +mean train EPE loss: 0.047915 +mean test EPE loss: 0.048381 +==epoch: 135== +mean train EPE loss: 0.051619 +mean test EPE loss: 0.054877 +==epoch: 136== +mean train EPE loss: 0.045060 +mean test EPE loss: 0.042842 +==epoch: 137== +mean train EPE loss: 0.042876 +mean test EPE loss: 0.042160 +==epoch: 138== +mean train EPE loss: 0.072831 +mean test EPE loss: 0.058927 +==epoch: 139== +mean train EPE loss: 0.047535 +mean test EPE loss: 0.052637 +==epoch: 140== +mean train EPE loss: 0.042772 +mean test EPE loss: 0.047589 +==epoch: 141== +mean train EPE loss: 0.067966 +mean test EPE loss: 0.080942 +==epoch: 142== +mean train EPE loss: 0.049519 +mean test EPE loss: 0.048909 +==epoch: 143== +mean train EPE loss: 0.057491 +mean test EPE loss: 0.045427 +==epoch: 144== +mean train EPE loss: 0.043909 +mean test EPE loss: 0.050124 +==epoch: 145== +mean train EPE loss: 0.040438 +mean test EPE loss: 0.065062 +==epoch: 146== +mean train EPE loss: 0.040756 +mean test EPE loss: 0.042820 +==epoch: 147== +mean train EPE loss: 0.039328 +mean test EPE loss: 0.045023 +==epoch: 148== +mean train EPE loss: 0.039731 +mean test EPE loss: 0.046295 +==epoch: 149== +mean train EPE loss: 0.033457 +mean test EPE loss: 0.038286 +==epoch: 150== +mean train EPE loss: 0.032474 +mean test EPE loss: 0.037882 +==epoch: 151== +mean train EPE loss: 0.032028 +mean test EPE loss: 0.036715 +best test loss till now: 0.036715 +==epoch: 152== +mean train EPE loss: 0.031789 +mean test EPE loss: 0.036179 +best test loss till now: 0.036179 +==epoch: 153== +mean train EPE loss: 0.031620 +mean test EPE loss: 0.036320 +==epoch: 154== +mean train EPE loss: 0.031462 +mean test EPE loss: 0.037848 +==epoch: 155== +mean train EPE loss: 0.031266 +mean test EPE loss: 0.035856 +best test loss till now: 0.035856 +==epoch: 156== +mean train EPE loss: 0.031153 +mean test EPE loss: 0.035330 +best test loss till now: 0.035330 +==epoch: 157== +mean train EPE loss: 0.030883 +mean test EPE loss: 0.038545 +==epoch: 158== +mean train EPE loss: 0.030771 +mean test EPE loss: 0.037662 +==epoch: 159== +mean train EPE loss: 0.030684 +mean test EPE loss: 0.036628 +==epoch: 160== +mean train EPE loss: 0.030658 +mean test EPE loss: 0.035776 +==epoch: 161== +mean train EPE loss: 0.030494 +mean test EPE loss: 0.036119 +==epoch: 162== +mean train EPE loss: 0.030485 +mean test EPE loss: 0.036775 +==epoch: 163== +mean train EPE loss: 0.030373 +mean test EPE loss: 0.036000 +==epoch: 164== +mean train EPE loss: 0.030155 +mean test EPE loss: 0.036485 +==epoch: 165== +mean train EPE loss: 0.030120 +mean test EPE loss: 0.037285 +==epoch: 166== +mean train EPE loss: 0.029916 +mean test EPE loss: 0.035390 +==epoch: 167== +mean train EPE loss: 0.030095 +mean test EPE loss: 0.035157 +best test loss till now: 0.035157 +==epoch: 168== +mean train EPE loss: 0.030084 +mean test EPE loss: 0.035677 +==epoch: 169== +mean train EPE loss: 0.029839 +mean test EPE loss: 0.037897 +==epoch: 170== +mean train EPE loss: 0.029642 +mean test EPE loss: 0.034786 +best test loss till now: 0.034786 +==epoch: 171== +mean train EPE loss: 0.029709 +mean test EPE loss: 0.035407 +==epoch: 172== +mean train EPE loss: 0.029876 +mean test EPE loss: 0.034901 +==epoch: 173== +mean train EPE loss: 0.029606 +mean test EPE loss: 0.034922 +==epoch: 174== +mean train EPE loss: 0.029639 +mean test EPE loss: 0.035265 +==epoch: 175== +mean train EPE loss: 0.029553 +mean test EPE loss: 0.034260 +best test loss till now: 0.034260 +==epoch: 176== +mean train EPE loss: 0.029447 +mean test EPE loss: 0.033920 +best test loss till now: 0.033920 +==epoch: 177== +mean train EPE loss: 0.029256 +mean test EPE loss: 0.034217 +==epoch: 178== +mean train EPE loss: 0.029407 +mean test EPE loss: 0.035980 +==epoch: 179== +mean train EPE loss: 0.029123 +mean test EPE loss: 0.035431 +==epoch: 180== +mean train EPE loss: 0.029134 +mean test EPE loss: 0.035094 +==epoch: 181== +mean train EPE loss: 0.029070 +mean test EPE loss: 0.033394 +best test loss till now: 0.033394 +==epoch: 182== +mean train EPE loss: 0.028978 +mean test EPE loss: 0.033072 +best test loss till now: 0.033072 +==epoch: 183== +mean train EPE loss: 0.029126 +mean test EPE loss: 0.033100 +==epoch: 184== +mean train EPE loss: 0.028822 +mean test EPE loss: 0.033545 +==epoch: 185== +mean train EPE loss: 0.028847 +mean test EPE loss: 0.035196 +==epoch: 186== +mean train EPE loss: 0.028828 +mean test EPE loss: 0.034903 +==epoch: 187== +mean train EPE loss: 0.028769 +mean test EPE loss: 0.033576 +==epoch: 188== +mean train EPE loss: 0.028639 +mean test EPE loss: 0.034419 +==epoch: 189== +mean train EPE loss: 0.028539 +mean test EPE loss: 0.033615 +==epoch: 190== +mean train EPE loss: 0.028608 +mean test EPE loss: 0.035396 +==epoch: 191== +mean train EPE loss: 0.028592 +mean test EPE loss: 0.033096 +==epoch: 192== +mean train EPE loss: 0.028420 +mean test EPE loss: 0.034238 +==epoch: 193== +mean train EPE loss: 0.028547 +mean test EPE loss: 0.034912 +==epoch: 194== +mean train EPE loss: 0.028453 +mean test EPE loss: 0.034301 +==epoch: 195== +mean train EPE loss: 0.028288 +mean test EPE loss: 0.034242 +==epoch: 196== +mean train EPE loss: 0.028220 +mean test EPE loss: 0.032635 +best test loss till now: 0.032635 +==epoch: 197== +mean train EPE loss: 0.028366 +mean test EPE loss: 0.032584 +best test loss till now: 0.032584 +==epoch: 198== +mean train EPE loss: 0.028457 +mean test EPE loss: 0.035276 +==epoch: 199== +mean train EPE loss: 0.027666 +mean test EPE loss: 0.034097 +==epoch: 200== +mean train EPE loss: 0.027626 +mean test EPE loss: 0.032592 +==epoch: 201== +mean train EPE loss: 0.027800 +mean test EPE loss: 0.032554 +best test loss till now: 0.032554 +==epoch: 202== +mean train EPE loss: 0.027502 +mean test EPE loss: 0.033208 +==epoch: 203== +mean train EPE loss: 0.027530 +mean test EPE loss: 0.034541 +==epoch: 204== +mean train EPE loss: 0.027374 +mean test EPE loss: 0.032156 +best test loss till now: 0.032156 +==epoch: 205== +mean train EPE loss: 0.027596 +mean test EPE loss: 0.032709 +==epoch: 206== +mean train EPE loss: 0.027474 +mean test EPE loss: 0.039307 +==epoch: 207== +mean train EPE loss: 0.027647 +mean test EPE loss: 0.032853 +==epoch: 208== +mean train EPE loss: 0.027440 +mean test EPE loss: 0.033836 +==epoch: 209== +mean train EPE loss: 0.027400 +mean test EPE loss: 0.033385 +==epoch: 210== +mean train EPE loss: 0.027556 +mean test EPE loss: 0.033443 +==epoch: 211== +mean train EPE loss: 0.027296 +mean test EPE loss: 0.033347 +==epoch: 212== +mean train EPE loss: 0.027284 +mean test EPE loss: 0.032211 +==epoch: 213== +mean train EPE loss: 0.027415 +mean test EPE loss: 0.032743 +==epoch: 214== +mean train EPE loss: 0.027309 +mean test EPE loss: 0.034585 +==epoch: 215== +mean train EPE loss: 0.027416 +mean test EPE loss: 0.033134 +==epoch: 216== +mean train EPE loss: 0.027519 +mean test EPE loss: 0.034241 +==epoch: 217== +mean train EPE loss: 0.027364 +mean test EPE loss: 0.032562 +==epoch: 218== +mean train EPE loss: 0.027416 +mean test EPE loss: 0.034114 +==epoch: 219== +mean train EPE loss: 0.027584 +mean test EPE loss: 0.036362 +==epoch: 220== +mean train EPE loss: 0.027331 +mean test EPE loss: 0.033793 +==epoch: 221== +mean train EPE loss: 0.027400 +mean test EPE loss: 0.033965 +==epoch: 222== +mean train EPE loss: 0.027401 +mean test EPE loss: 0.034118 +==epoch: 223== +mean train EPE loss: 0.027484 +mean test EPE loss: 0.033143 +==epoch: 224== +mean train EPE loss: 0.027273 +mean test EPE loss: 0.032555 +==epoch: 225== +mean train EPE loss: 0.027389 +mean test EPE loss: 0.031877 +best test loss till now: 0.031877 +==epoch: 226== +mean train EPE loss: 0.027403 +mean test EPE loss: 0.032648 +==epoch: 227== +mean train EPE loss: 0.027210 +mean test EPE loss: 0.033452 +==epoch: 228== +mean train EPE loss: 0.027269 +mean test EPE loss: 0.034084 +==epoch: 229== +mean train EPE loss: 0.027530 +mean test EPE loss: 0.033470 +==epoch: 230== +mean train EPE loss: 0.027243 +mean test EPE loss: 0.032014 +==epoch: 231== +mean train EPE loss: 0.027258 +mean test EPE loss: 0.032905 +==epoch: 232== +mean train EPE loss: 0.027289 +mean test EPE loss: 0.033180 +==epoch: 233== +mean train EPE loss: 0.027176 +mean test EPE loss: 0.033628 +==epoch: 234== +mean train EPE loss: 0.027346 +mean test EPE loss: 0.033026 +==epoch: 235== +mean train EPE loss: 0.027260 +mean test EPE loss: 0.032652 +==epoch: 236== +mean train EPE loss: 0.027303 +mean test EPE loss: 0.032143 +==epoch: 237== +mean train EPE loss: 0.027373 +mean test EPE loss: 0.032356 +==epoch: 238== +mean train EPE loss: 0.027216 +mean test EPE loss: 0.033173 +==epoch: 239== +mean train EPE loss: 0.027255 +mean test EPE loss: 0.032314 +==epoch: 240== +mean train EPE loss: 0.027256 +mean test EPE loss: 0.031535 +best test loss till now: 0.031535 +==epoch: 241== +mean train EPE loss: 0.027334 +mean test EPE loss: 0.032058 +==epoch: 242== +mean train EPE loss: 0.027234 +mean test EPE loss: 0.032868 +==epoch: 243== +mean train EPE loss: 0.027290 +mean test EPE loss: 0.033477 +==epoch: 244== +mean train EPE loss: 0.027293 +mean test EPE loss: 0.032572 +==epoch: 245== +mean train EPE loss: 0.027128 +mean test EPE loss: 0.032159 +==epoch: 246== +mean train EPE loss: 0.027262 +mean test EPE loss: 0.032254 +==epoch: 247== +mean train EPE loss: 0.027206 +mean test EPE loss: 0.033084 +==epoch: 248== +mean train EPE loss: 0.027257 +mean test EPE loss: 0.031839 +==epoch: 249== +mean train EPE loss: 0.027205 +mean test EPE loss: 0.031678 diff --git a/thirdparty/learning3d/pretrained/exp_ipcrnet/events.out.tfevents.1584152656.bioroboticslab b/thirdparty/learning3d/pretrained/exp_ipcrnet/events.out.tfevents.1584152656.bioroboticslab new file mode 100644 index 0000000000000000000000000000000000000000..6fe83bf8a9470d3ec69e9df8cb285eac9a9e0605 --- /dev/null +++ b/thirdparty/learning3d/pretrained/exp_ipcrnet/events.out.tfevents.1584152656.bioroboticslab @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9daaf8c6b240c1efe5cb1a2ffc9d05a746cb625b0220e656f0cbc6316c6389d1 +size 29509 diff --git a/thirdparty/learning3d/pretrained/exp_ipcrnet/models/best_model.t7 b/thirdparty/learning3d/pretrained/exp_ipcrnet/models/best_model.t7 new file mode 100644 index 0000000000000000000000000000000000000000..ed01d4b2f818c17038fb3b5e9f29dd6fd78b00c7 --- /dev/null +++ b/thirdparty/learning3d/pretrained/exp_ipcrnet/models/best_model.t7 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:078a15d25f0c802127ebd9269b14b407fa3d1c60376bcfe43bb07e093db027b4 +size 16872942 diff --git a/thirdparty/learning3d/pretrained/exp_ipcrnet/models/best_model_v1.t7 b/thirdparty/learning3d/pretrained/exp_ipcrnet/models/best_model_v1.t7 new file mode 100644 index 0000000000000000000000000000000000000000..8ca5e14e06e65ac5bb178866aef4e62832d3b7c5 --- /dev/null +++ b/thirdparty/learning3d/pretrained/exp_ipcrnet/models/best_model_v1.t7 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8d59707a47c33e737865684661d4aba125f4d1710a7a624087114877dbd232da +size 16872893 diff --git a/thirdparty/learning3d/pretrained/exp_ipcrnet/models/best_ptnet_model.t7 b/thirdparty/learning3d/pretrained/exp_ipcrnet/models/best_ptnet_model.t7 new file mode 100644 index 0000000000000000000000000000000000000000..b97f14fe2584886e8adac59348bb5f04d33369f0 --- /dev/null +++ b/thirdparty/learning3d/pretrained/exp_ipcrnet/models/best_ptnet_model.t7 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:078285366022134cdd5c4f97fe9f3967f2e57d3d763a8cfa732f72a62d250285 +size 597509 diff --git a/thirdparty/learning3d/pretrained/exp_ipcrnet/run.log b/thirdparty/learning3d/pretrained/exp_ipcrnet/run.log new file mode 100644 index 0000000000000000000000000000000000000000..0e68405deec0f4911ec81fc439461e4bc42dbb80 --- /dev/null +++ b/thirdparty/learning3d/pretrained/exp_ipcrnet/run.log @@ -0,0 +1,201 @@ +Namespace(batch_size=20, dataset_path='ModelNet40', dataset_type='modelnet', device='cuda:0', emb_dims=1024, epochs=200, eval=False, exp_name='exp_ipcrnet', num_points=1024, optimizer='Adam', pointnet='tune', pretrained='', resume='', seed=1234, start_epoch=0, symfn='max', workers=4) +EPOCH:: 1, Traininig Loss: 0.132034, Testing Loss: 0.113057, Best Loss: 0.113057 +EPOCH:: 2, Traininig Loss: 0.094734, Testing Loss: 0.080374, Best Loss: 0.080374 +EPOCH:: 3, Traininig Loss: 0.072277, Testing Loss: 0.061047, Best Loss: 0.061047 +EPOCH:: 4, Traininig Loss: 0.061098, Testing Loss: 0.048089, Best Loss: 0.048089 +EPOCH:: 5, Traininig Loss: 0.055146, Testing Loss: 0.056887, Best Loss: 0.048089 +EPOCH:: 6, Traininig Loss: 0.049294, Testing Loss: 0.047112, Best Loss: 0.047112 +EPOCH:: 7, Traininig Loss: 0.045512, Testing Loss: 0.041145, Best Loss: 0.041145 +EPOCH:: 8, Traininig Loss: 0.041171, Testing Loss: 0.045314, Best Loss: 0.041145 +EPOCH:: 9, Traininig Loss: 0.050243, Testing Loss: 0.100156, Best Loss: 0.041145 +EPOCH:: 10, Traininig Loss: 0.070933, Testing Loss: 0.125304, Best Loss: 0.041145 +EPOCH:: 11, Traininig Loss: 0.113713, Testing Loss: 0.106984, Best Loss: 0.041145 +EPOCH:: 12, Traininig Loss: 0.109325, Testing Loss: 0.104972, Best Loss: 0.041145 +EPOCH:: 13, Traininig Loss: 0.108790, Testing Loss: 0.107601, Best Loss: 0.041145 +EPOCH:: 14, Traininig Loss: 0.106146, Testing Loss: 0.104348, Best Loss: 0.041145 +EPOCH:: 15, Traininig Loss: 0.103300, Testing Loss: 0.098483, Best Loss: 0.041145 +EPOCH:: 16, Traininig Loss: 0.102242, Testing Loss: 0.102602, Best Loss: 0.041145 +EPOCH:: 17, Traininig Loss: 0.115175, Testing Loss: 0.107133, Best Loss: 0.041145 +EPOCH:: 18, Traininig Loss: 0.106395, Testing Loss: 0.101851, Best Loss: 0.041145 +EPOCH:: 19, Traininig Loss: 0.104768, Testing Loss: 0.101792, Best Loss: 0.041145 +EPOCH:: 20, Traininig Loss: 0.105212, Testing Loss: 0.099475, Best Loss: 0.041145 +EPOCH:: 21, Traininig Loss: 0.100713, Testing Loss: 0.095733, Best Loss: 0.041145 +EPOCH:: 22, Traininig Loss: 0.120899, Testing Loss: 0.056937, Best Loss: 0.041145 +EPOCH:: 23, Traininig Loss: 0.048538, Testing Loss: 0.040574, Best Loss: 0.040574 +EPOCH:: 24, Traininig Loss: 0.039571, Testing Loss: 0.035685, Best Loss: 0.035685 +EPOCH:: 25, Traininig Loss: 0.036247, Testing Loss: 0.031603, Best Loss: 0.031603 +EPOCH:: 26, Traininig Loss: 0.047465, Testing Loss: 0.034926, Best Loss: 0.031603 +EPOCH:: 27, Traininig Loss: 0.033182, Testing Loss: 0.032107, Best Loss: 0.031603 +EPOCH:: 28, Traininig Loss: 0.035900, Testing Loss: 0.030397, Best Loss: 0.030397 +EPOCH:: 29, Traininig Loss: 0.031767, Testing Loss: 0.038034, Best Loss: 0.030397 +EPOCH:: 30, Traininig Loss: 0.036039, Testing Loss: 0.028127, Best Loss: 0.028127 +EPOCH:: 31, Traininig Loss: 0.031742, Testing Loss: 0.039371, Best Loss: 0.028127 +EPOCH:: 32, Traininig Loss: 0.033824, Testing Loss: 0.036018, Best Loss: 0.028127 +EPOCH:: 33, Traininig Loss: 0.049689, Testing Loss: 0.030172, Best Loss: 0.028127 +EPOCH:: 34, Traininig Loss: 0.029778, Testing Loss: 0.032157, Best Loss: 0.028127 +EPOCH:: 35, Traininig Loss: 0.038241, Testing Loss: 0.036507, Best Loss: 0.028127 +EPOCH:: 36, Traininig Loss: 0.033978, Testing Loss: 0.029606, Best Loss: 0.028127 +EPOCH:: 37, Traininig Loss: 0.027237, Testing Loss: 0.026675, Best Loss: 0.026675 +EPOCH:: 38, Traininig Loss: 0.029019, Testing Loss: 0.026410, Best Loss: 0.026410 +EPOCH:: 39, Traininig Loss: 0.026281, Testing Loss: 0.023359, Best Loss: 0.023359 +EPOCH:: 40, Traininig Loss: 0.029383, Testing Loss: 0.028541, Best Loss: 0.023359 +EPOCH:: 41, Traininig Loss: 0.026708, Testing Loss: 0.024856, Best Loss: 0.023359 +EPOCH:: 42, Traininig Loss: 0.023211, Testing Loss: 0.022710, Best Loss: 0.022710 +EPOCH:: 43, Traininig Loss: 0.026940, Testing Loss: 0.026355, Best Loss: 0.022710 +EPOCH:: 44, Traininig Loss: 0.025613, Testing Loss: 0.054558, Best Loss: 0.022710 +EPOCH:: 45, Traininig Loss: 0.030651, Testing Loss: 0.025855, Best Loss: 0.022710 +EPOCH:: 46, Traininig Loss: 0.023726, Testing Loss: 0.025550, Best Loss: 0.022710 +EPOCH:: 47, Traininig Loss: 0.030620, Testing Loss: 0.029232, Best Loss: 0.022710 +EPOCH:: 48, Traininig Loss: 0.026301, Testing Loss: 0.027510, Best Loss: 0.022710 +EPOCH:: 49, Traininig Loss: 0.024818, Testing Loss: 0.030148, Best Loss: 0.022710 +EPOCH:: 50, Traininig Loss: 0.022924, Testing Loss: 0.028365, Best Loss: 0.022710 +EPOCH:: 51, Traininig Loss: 0.024954, Testing Loss: 0.036872, Best Loss: 0.022710 +EPOCH:: 52, Traininig Loss: 0.024056, Testing Loss: 0.022789, Best Loss: 0.022710 +EPOCH:: 53, Traininig Loss: 0.021554, Testing Loss: 0.021131, Best Loss: 0.021131 +EPOCH:: 54, Traininig Loss: 0.024028, Testing Loss: 0.025700, Best Loss: 0.021131 +EPOCH:: 55, Traininig Loss: 0.031103, Testing Loss: 0.024593, Best Loss: 0.021131 +EPOCH:: 56, Traininig Loss: 0.033893, Testing Loss: 0.035062, Best Loss: 0.021131 +EPOCH:: 57, Traininig Loss: 0.028645, Testing Loss: 0.021254, Best Loss: 0.021131 +EPOCH:: 58, Traininig Loss: 0.027861, Testing Loss: 0.034598, Best Loss: 0.021131 +EPOCH:: 59, Traininig Loss: 0.028774, Testing Loss: 0.021490, Best Loss: 0.021131 +EPOCH:: 60, Traininig Loss: 0.020787, Testing Loss: 0.020750, Best Loss: 0.020750 +EPOCH:: 61, Traininig Loss: 0.024709, Testing Loss: 0.027572, Best Loss: 0.020750 +EPOCH:: 62, Traininig Loss: 0.022285, Testing Loss: 0.020819, Best Loss: 0.020750 +EPOCH:: 63, Traininig Loss: 0.025234, Testing Loss: 0.024038, Best Loss: 0.020750 +EPOCH:: 64, Traininig Loss: 0.021017, Testing Loss: 0.019929, Best Loss: 0.019929 +EPOCH:: 65, Traininig Loss: 0.025015, Testing Loss: 0.025491, Best Loss: 0.019929 +EPOCH:: 66, Traininig Loss: 0.028033, Testing Loss: 0.024362, Best Loss: 0.019929 +EPOCH:: 67, Traininig Loss: 0.025978, Testing Loss: 0.023088, Best Loss: 0.019929 +EPOCH:: 68, Traininig Loss: 0.024941, Testing Loss: 0.019315, Best Loss: 0.019315 +EPOCH:: 69, Traininig Loss: 0.036310, Testing Loss: 0.030028, Best Loss: 0.019315 +EPOCH:: 70, Traininig Loss: 0.024219, Testing Loss: 0.021006, Best Loss: 0.019315 +EPOCH:: 71, Traininig Loss: 0.021298, Testing Loss: 0.023795, Best Loss: 0.019315 +EPOCH:: 72, Traininig Loss: 0.021578, Testing Loss: 0.019311, Best Loss: 0.019311 +EPOCH:: 73, Traininig Loss: 0.018863, Testing Loss: 0.018013, Best Loss: 0.018013 +EPOCH:: 74, Traininig Loss: 0.018586, Testing Loss: 0.019757, Best Loss: 0.018013 +EPOCH:: 75, Traininig Loss: 0.022397, Testing Loss: 0.017982, Best Loss: 0.017982 +EPOCH:: 76, Traininig Loss: 0.019164, Testing Loss: 0.017920, Best Loss: 0.017920 +EPOCH:: 77, Traininig Loss: 0.018671, Testing Loss: 0.018917, Best Loss: 0.017920 +EPOCH:: 78, Traininig Loss: 0.020246, Testing Loss: 0.021040, Best Loss: 0.017920 +EPOCH:: 79, Traininig Loss: 0.020542, Testing Loss: 0.018637, Best Loss: 0.017920 +EPOCH:: 80, Traininig Loss: 0.024035, Testing Loss: 0.182431, Best Loss: 0.017920 +EPOCH:: 81, Traininig Loss: 0.051901, Testing Loss: 0.033691, Best Loss: 0.017920 +EPOCH:: 82, Traininig Loss: 0.031150, Testing Loss: 0.026755, Best Loss: 0.017920 +EPOCH:: 83, Traininig Loss: 0.025962, Testing Loss: 0.025284, Best Loss: 0.017920 +EPOCH:: 84, Traininig Loss: 0.024523, Testing Loss: 0.022102, Best Loss: 0.017920 +EPOCH:: 85, Traininig Loss: 0.022505, Testing Loss: 0.020574, Best Loss: 0.017920 +EPOCH:: 86, Traininig Loss: 0.020485, Testing Loss: 0.020584, Best Loss: 0.017920 +EPOCH:: 87, Traininig Loss: 0.020988, Testing Loss: 0.022133, Best Loss: 0.017920 +EPOCH:: 88, Traininig Loss: 0.019630, Testing Loss: 0.018773, Best Loss: 0.017920 +EPOCH:: 89, Traininig Loss: 0.019907, Testing Loss: 0.020470, Best Loss: 0.017920 +EPOCH:: 90, Traininig Loss: 0.019992, Testing Loss: 0.019482, Best Loss: 0.017920 +EPOCH:: 91, Traininig Loss: 0.019514, Testing Loss: 0.020086, Best Loss: 0.017920 +EPOCH:: 92, Traininig Loss: 0.025225, Testing Loss: 0.019700, Best Loss: 0.017920 +EPOCH:: 93, Traininig Loss: 0.020192, Testing Loss: 0.037905, Best Loss: 0.017920 +EPOCH:: 94, Traininig Loss: 0.020653, Testing Loss: 0.017289, Best Loss: 0.017289 +EPOCH:: 95, Traininig Loss: 0.018758, Testing Loss: 0.022545, Best Loss: 0.017289 +EPOCH:: 96, Traininig Loss: 0.021325, Testing Loss: 0.019542, Best Loss: 0.017289 +EPOCH:: 97, Traininig Loss: 0.019205, Testing Loss: 0.017471, Best Loss: 0.017289 +EPOCH:: 98, Traininig Loss: 0.021222, Testing Loss: 0.024184, Best Loss: 0.017289 +EPOCH:: 99, Traininig Loss: 0.078757, Testing Loss: 0.090866, Best Loss: 0.017289 +EPOCH:: 100, Traininig Loss: 0.087211, Testing Loss: 0.079484, Best Loss: 0.017289 +EPOCH:: 101, Traininig Loss: 0.079988, Testing Loss: 0.076940, Best Loss: 0.017289 +EPOCH:: 102, Traininig Loss: 0.077231, Testing Loss: 0.078331, Best Loss: 0.017289 +EPOCH:: 103, Traininig Loss: 0.074265, Testing Loss: 0.071676, Best Loss: 0.017289 +EPOCH:: 104, Traininig Loss: 0.071649, Testing Loss: 0.069539, Best Loss: 0.017289 +EPOCH:: 105, Traininig Loss: 0.073833, Testing Loss: 0.069991, Best Loss: 0.017289 +EPOCH:: 106, Traininig Loss: 0.069042, Testing Loss: 0.067904, Best Loss: 0.017289 +EPOCH:: 107, Traininig Loss: 0.067840, Testing Loss: 0.071509, Best Loss: 0.017289 +EPOCH:: 108, Traininig Loss: 0.066670, Testing Loss: 0.068162, Best Loss: 0.017289 +EPOCH:: 109, Traininig Loss: 0.067506, Testing Loss: 0.067412, Best Loss: 0.017289 +EPOCH:: 110, Traininig Loss: 0.073742, Testing Loss: 0.064510, Best Loss: 0.017289 +EPOCH:: 111, Traininig Loss: 0.065212, Testing Loss: 0.069884, Best Loss: 0.017289 +EPOCH:: 112, Traininig Loss: 0.063429, Testing Loss: 0.076627, Best Loss: 0.017289 +EPOCH:: 113, Traininig Loss: 0.065861, Testing Loss: 0.061395, Best Loss: 0.017289 +EPOCH:: 114, Traininig Loss: 0.076420, Testing Loss: 0.072040, Best Loss: 0.017289 +EPOCH:: 115, Traininig Loss: 0.068562, Testing Loss: 0.068978, Best Loss: 0.017289 +EPOCH:: 116, Traininig Loss: 0.072725, Testing Loss: 0.064668, Best Loss: 0.017289 +EPOCH:: 117, Traininig Loss: 0.070741, Testing Loss: 0.069425, Best Loss: 0.017289 +EPOCH:: 118, Traininig Loss: 0.063055, Testing Loss: 0.073741, Best Loss: 0.017289 +EPOCH:: 119, Traininig Loss: 0.063276, Testing Loss: 0.060796, Best Loss: 0.017289 +EPOCH:: 120, Traininig Loss: 0.071687, Testing Loss: 0.064952, Best Loss: 0.017289 +EPOCH:: 121, Traininig Loss: 0.065865, Testing Loss: 0.064840, Best Loss: 0.017289 +EPOCH:: 122, Traininig Loss: 0.079595, Testing Loss: 0.131784, Best Loss: 0.017289 +EPOCH:: 123, Traininig Loss: 0.082287, Testing Loss: 0.074767, Best Loss: 0.017289 +EPOCH:: 124, Traininig Loss: 0.073918, Testing Loss: 0.087813, Best Loss: 0.017289 +EPOCH:: 125, Traininig Loss: 0.076475, Testing Loss: 0.067773, Best Loss: 0.017289 +EPOCH:: 126, Traininig Loss: 0.086541, Testing Loss: 0.084243, Best Loss: 0.017289 +EPOCH:: 127, Traininig Loss: 0.079331, Testing Loss: 0.074983, Best Loss: 0.017289 +EPOCH:: 128, Traininig Loss: 0.079441, Testing Loss: 0.081553, Best Loss: 0.017289 +EPOCH:: 129, Traininig Loss: 0.073699, Testing Loss: 0.071399, Best Loss: 0.017289 +EPOCH:: 130, Traininig Loss: 0.070517, Testing Loss: 0.069885, Best Loss: 0.017289 +EPOCH:: 131, Traininig Loss: 0.071485, Testing Loss: 0.077261, Best Loss: 0.017289 +EPOCH:: 132, Traininig Loss: 0.073561, Testing Loss: 0.081381, Best Loss: 0.017289 +EPOCH:: 133, Traininig Loss: 0.070543, Testing Loss: 0.070513, Best Loss: 0.017289 +EPOCH:: 134, Traininig Loss: 0.065431, Testing Loss: 0.070795, Best Loss: 0.017289 +EPOCH:: 135, Traininig Loss: 0.067365, Testing Loss: 0.085788, Best Loss: 0.017289 +EPOCH:: 136, Traininig Loss: 0.079378, Testing Loss: 0.071349, Best Loss: 0.017289 +EPOCH:: 137, Traininig Loss: 0.069665, Testing Loss: 0.068003, Best Loss: 0.017289 +EPOCH:: 138, Traininig Loss: 0.068529, Testing Loss: 0.072463, Best Loss: 0.017289 +EPOCH:: 139, Traininig Loss: 0.095010, Testing Loss: 0.076060, Best Loss: 0.017289 +EPOCH:: 140, Traininig Loss: 0.076035, Testing Loss: 0.073614, Best Loss: 0.017289 +EPOCH:: 141, Traininig Loss: 0.073773, Testing Loss: 0.073914, Best Loss: 0.017289 +EPOCH:: 142, Traininig Loss: 0.074592, Testing Loss: 0.071991, Best Loss: 0.017289 +EPOCH:: 143, Traininig Loss: 0.073327, Testing Loss: 0.075837, Best Loss: 0.017289 +EPOCH:: 144, Traininig Loss: 0.072188, Testing Loss: 0.069251, Best Loss: 0.017289 +EPOCH:: 145, Traininig Loss: 0.070235, Testing Loss: 0.076869, Best Loss: 0.017289 +EPOCH:: 146, Traininig Loss: 0.069487, Testing Loss: 0.066857, Best Loss: 0.017289 +EPOCH:: 147, Traininig Loss: 0.064024, Testing Loss: 0.064125, Best Loss: 0.017289 +EPOCH:: 148, Traininig Loss: 0.067837, Testing Loss: 0.065485, Best Loss: 0.017289 +EPOCH:: 149, Traininig Loss: 0.066247, Testing Loss: 0.068113, Best Loss: 0.017289 +EPOCH:: 150, Traininig Loss: 0.067933, Testing Loss: 0.065105, Best Loss: 0.017289 +EPOCH:: 151, Traininig Loss: 0.065855, Testing Loss: 0.064639, Best Loss: 0.017289 +EPOCH:: 152, Traininig Loss: 0.066654, Testing Loss: 0.070741, Best Loss: 0.017289 +EPOCH:: 153, Traininig Loss: 0.066591, Testing Loss: 0.064588, Best Loss: 0.017289 +EPOCH:: 154, Traininig Loss: 0.060339, Testing Loss: 0.061429, Best Loss: 0.017289 +EPOCH:: 155, Traininig Loss: 0.061685, Testing Loss: 0.057667, Best Loss: 0.017289 +EPOCH:: 156, Traininig Loss: 0.071250, Testing Loss: 0.064288, Best Loss: 0.017289 +EPOCH:: 157, Traininig Loss: 0.106632, Testing Loss: 0.084811, Best Loss: 0.017289 +EPOCH:: 158, Traininig Loss: 0.083257, Testing Loss: 0.079746, Best Loss: 0.017289 +EPOCH:: 159, Traininig Loss: 0.080007, Testing Loss: 0.077464, Best Loss: 0.017289 +EPOCH:: 160, Traininig Loss: 0.077360, Testing Loss: 0.074688, Best Loss: 0.017289 +EPOCH:: 161, Traininig Loss: 0.075625, Testing Loss: 0.073910, Best Loss: 0.017289 +EPOCH:: 162, Traininig Loss: 0.075468, Testing Loss: 0.074161, Best Loss: 0.017289 +EPOCH:: 163, Traininig Loss: 0.073686, Testing Loss: 0.072340, Best Loss: 0.017289 +EPOCH:: 164, Traininig Loss: 0.084558, Testing Loss: 0.077605, Best Loss: 0.017289 +EPOCH:: 165, Traininig Loss: 0.075914, Testing Loss: 0.073363, Best Loss: 0.017289 +EPOCH:: 166, Traininig Loss: 0.073102, Testing Loss: 0.072103, Best Loss: 0.017289 +EPOCH:: 167, Traininig Loss: 0.068739, Testing Loss: 0.067346, Best Loss: 0.017289 +EPOCH:: 168, Traininig Loss: 0.065666, Testing Loss: 0.068959, Best Loss: 0.017289 +EPOCH:: 169, Traininig Loss: 0.071427, Testing Loss: 0.069914, Best Loss: 0.017289 +EPOCH:: 170, Traininig Loss: 0.069501, Testing Loss: 0.074415, Best Loss: 0.017289 +EPOCH:: 171, Traininig Loss: 0.066508, Testing Loss: 0.066623, Best Loss: 0.017289 +EPOCH:: 172, Traininig Loss: 0.066848, Testing Loss: 0.063161, Best Loss: 0.017289 +EPOCH:: 173, Traininig Loss: 0.062455, Testing Loss: 0.063608, Best Loss: 0.017289 +EPOCH:: 174, Traininig Loss: 0.067098, Testing Loss: 0.062732, Best Loss: 0.017289 +EPOCH:: 175, Traininig Loss: 0.063088, Testing Loss: 0.057446, Best Loss: 0.017289 +EPOCH:: 176, Traininig Loss: 0.099322, Testing Loss: 0.084470, Best Loss: 0.017289 +EPOCH:: 177, Traininig Loss: 0.082811, Testing Loss: 0.077278, Best Loss: 0.017289 +EPOCH:: 178, Traininig Loss: 0.078180, Testing Loss: 0.076911, Best Loss: 0.017289 +EPOCH:: 179, Traininig Loss: 0.077092, Testing Loss: 0.074643, Best Loss: 0.017289 +EPOCH:: 180, Traininig Loss: 0.075774, Testing Loss: 0.075513, Best Loss: 0.017289 +EPOCH:: 181, Traininig Loss: 0.074817, Testing Loss: 0.074516, Best Loss: 0.017289 +EPOCH:: 182, Traininig Loss: 0.074467, Testing Loss: 0.073850, Best Loss: 0.017289 +EPOCH:: 183, Traininig Loss: 0.073365, Testing Loss: 0.073253, Best Loss: 0.017289 +EPOCH:: 184, Traininig Loss: 0.098085, Testing Loss: 0.130011, Best Loss: 0.017289 +EPOCH:: 185, Traininig Loss: 0.124823, Testing Loss: 0.119678, Best Loss: 0.017289 +EPOCH:: 186, Traininig Loss: 0.111775, Testing Loss: 0.113932, Best Loss: 0.017289 +EPOCH:: 187, Traininig Loss: 0.111354, Testing Loss: 0.109733, Best Loss: 0.017289 +EPOCH:: 188, Traininig Loss: 0.111613, Testing Loss: 0.113106, Best Loss: 0.017289 +EPOCH:: 189, Traininig Loss: 0.088431, Testing Loss: 0.082188, Best Loss: 0.017289 +EPOCH:: 190, Traininig Loss: 0.078025, Testing Loss: 0.075767, Best Loss: 0.017289 +EPOCH:: 191, Traininig Loss: 0.075403, Testing Loss: 0.072506, Best Loss: 0.017289 +EPOCH:: 192, Traininig Loss: 0.081848, Testing Loss: 0.243384, Best Loss: 0.017289 +EPOCH:: 193, Traininig Loss: 0.097957, Testing Loss: 0.081948, Best Loss: 0.017289 +EPOCH:: 194, Traininig Loss: 0.081963, Testing Loss: 0.078019, Best Loss: 0.017289 +EPOCH:: 195, Traininig Loss: 0.077585, Testing Loss: 0.075284, Best Loss: 0.017289 +EPOCH:: 196, Traininig Loss: 0.075390, Testing Loss: 0.072978, Best Loss: 0.017289 +EPOCH:: 197, Traininig Loss: 0.073288, Testing Loss: 0.071345, Best Loss: 0.017289 +EPOCH:: 198, Traininig Loss: 0.071696, Testing Loss: 0.070100, Best Loss: 0.017289 +EPOCH:: 199, Traininig Loss: 0.080471, Testing Loss: 0.074226, Best Loss: 0.017289 +EPOCH:: 200, Traininig Loss: 0.079577, Testing Loss: 0.073731, Best Loss: 0.017289 diff --git a/thirdparty/learning3d/pretrained/exp_masknet/models/best_model.t7 b/thirdparty/learning3d/pretrained/exp_masknet/models/best_model.t7 new file mode 100644 index 0000000000000000000000000000000000000000..01e3e1e031243f42ac52f2d9ed4574b8a6df013f --- /dev/null +++ b/thirdparty/learning3d/pretrained/exp_masknet/models/best_model.t7 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f2f44cacc6f7a9c1bc94994fe3944f52333b5fb6eaf45a5e8a3d6757da898ab2 +size 11775115 diff --git a/thirdparty/learning3d/pretrained/exp_masknet2/models/best_model_0.01.t7 b/thirdparty/learning3d/pretrained/exp_masknet2/models/best_model_0.01.t7 new file mode 100644 index 0000000000000000000000000000000000000000..5503e7393357062b65f53f182f295f274888bf85 --- /dev/null +++ b/thirdparty/learning3d/pretrained/exp_masknet2/models/best_model_0.01.t7 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:75652860b60c16bde755fa16902ef37f6cd49942e0015c492693080fd806f17b +size 6144283 diff --git a/thirdparty/learning3d/pretrained/exp_masknet2/models/best_model_0.6.t7 b/thirdparty/learning3d/pretrained/exp_masknet2/models/best_model_0.6.t7 new file mode 100644 index 0000000000000000000000000000000000000000..de9304b4eb27fe3fd27708c2e89179053b94a9ab --- /dev/null +++ b/thirdparty/learning3d/pretrained/exp_masknet2/models/best_model_0.6.t7 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fc916052dd3779a796a14e071acfe48329011bdb91aeb154639875ada42fb7b2 +size 6144283 diff --git a/thirdparty/learning3d/pretrained/exp_masknet2/models/best_model_0.7.t7 b/thirdparty/learning3d/pretrained/exp_masknet2/models/best_model_0.7.t7 new file mode 100644 index 0000000000000000000000000000000000000000..56f110eaec495d1c4563ac8cc75b5a86d57fe31f --- /dev/null +++ b/thirdparty/learning3d/pretrained/exp_masknet2/models/best_model_0.7.t7 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6e6389cdfa32fa90510bba51eeb6854def70c7a2b49d4fbbcace69a036f402c9 +size 6144283 diff --git a/thirdparty/learning3d/pretrained/exp_masknet2/models/best_model_0.8.t7 b/thirdparty/learning3d/pretrained/exp_masknet2/models/best_model_0.8.t7 new file mode 100644 index 0000000000000000000000000000000000000000..965a69f54a3798886f7cf634b4d40d5c77d4644e --- /dev/null +++ b/thirdparty/learning3d/pretrained/exp_masknet2/models/best_model_0.8.t7 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ede68860d05c5372105ae9a0460de3a35ce1666196cd9824a8b64e048d0a8a5 +size 6144283 diff --git a/thirdparty/learning3d/pretrained/exp_masknet2/models/best_model_0.9.t7 b/thirdparty/learning3d/pretrained/exp_masknet2/models/best_model_0.9.t7 new file mode 100644 index 0000000000000000000000000000000000000000..19f278b5aa06b197f96ed42dea1bc4907b31a22d --- /dev/null +++ b/thirdparty/learning3d/pretrained/exp_masknet2/models/best_model_0.9.t7 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:52b3a7e91d0902c0523fde21d67eaf33f8d737fb00d3bf9d19859ccd3a465640 +size 6144283 diff --git a/thirdparty/learning3d/pretrained/exp_masknet2/models/best_model_100.t7 b/thirdparty/learning3d/pretrained/exp_masknet2/models/best_model_100.t7 new file mode 100644 index 0000000000000000000000000000000000000000..51677a09b0669ca045b8cd4ca32bd14d2e887963 --- /dev/null +++ b/thirdparty/learning3d/pretrained/exp_masknet2/models/best_model_100.t7 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:404d38714181e51b32f5b610f5a788caf6ef7af92453061dd7076f360f2ac487 +size 6144283 diff --git a/thirdparty/learning3d/pretrained/exp_masknet2/models/best_model_200.t7 b/thirdparty/learning3d/pretrained/exp_masknet2/models/best_model_200.t7 new file mode 100644 index 0000000000000000000000000000000000000000..a315013f8f81819b75acbea6bc728f4da8e45058 --- /dev/null +++ b/thirdparty/learning3d/pretrained/exp_masknet2/models/best_model_200.t7 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a4f4bdfd6eeaea8dbf9901b5ada8004b1c464774620e06e49f416dd9943e46df +size 6144283 diff --git a/thirdparty/learning3d/pretrained/exp_masknet2/models/best_model_300.t7 b/thirdparty/learning3d/pretrained/exp_masknet2/models/best_model_300.t7 new file mode 100644 index 0000000000000000000000000000000000000000..d9ff865a79373babf3ccdbbb84aaf51f6d4b8405 --- /dev/null +++ b/thirdparty/learning3d/pretrained/exp_masknet2/models/best_model_300.t7 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3c8f589e72d5bcd1edaa7cf41c4011c9e6ea777a324ffc238e26a5860cc18875 +size 6144283 diff --git a/thirdparty/learning3d/pretrained/exp_masknet2/models/best_model_400.t7 b/thirdparty/learning3d/pretrained/exp_masknet2/models/best_model_400.t7 new file mode 100644 index 0000000000000000000000000000000000000000..151e4cc89ab88601362ab622b1f586060a9942e0 --- /dev/null +++ b/thirdparty/learning3d/pretrained/exp_masknet2/models/best_model_400.t7 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7f9ed797a817e72af45dfcb983e138201143d268352313ab7ec9b6aff7ad8817 +size 6144283 diff --git a/thirdparty/learning3d/pretrained/exp_masknet2/models/best_model_500.t7 b/thirdparty/learning3d/pretrained/exp_masknet2/models/best_model_500.t7 new file mode 100644 index 0000000000000000000000000000000000000000..4b2301a629b6ca836d517309215605ffe0b4a0c2 --- /dev/null +++ b/thirdparty/learning3d/pretrained/exp_masknet2/models/best_model_500.t7 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:45b79d08fedd7c73056f8c9b02af49c6e648e6966ba1b751f88f803795b609cb +size 6144283 diff --git a/thirdparty/learning3d/pretrained/exp_pcn/events.out.tfevents.1584988327.lambda-dual b/thirdparty/learning3d/pretrained/exp_pcn/events.out.tfevents.1584988327.lambda-dual new file mode 100644 index 0000000000000000000000000000000000000000..a701bf889b867985e7900a8bf96308b2fcd0836d --- /dev/null +++ b/thirdparty/learning3d/pretrained/exp_pcn/events.out.tfevents.1584988327.lambda-dual @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19327044bd7368a4aaf505bc93e61e784eed6fd941e31ea8cb2bad3c36146d6c +size 29059 diff --git a/thirdparty/learning3d/pretrained/exp_pcn/models/best_model.t7 b/thirdparty/learning3d/pretrained/exp_pcn/models/best_model.t7 new file mode 100644 index 0000000000000000000000000000000000000000..0321bd74b2e7aa625565f8a4d8bc6617b4aa65c8 --- /dev/null +++ b/thirdparty/learning3d/pretrained/exp_pcn/models/best_model.t7 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:566bdc1e5e7aa814380e881272bee971d49a16a729cf7817d4f9b276fbfa28e9 +size 24280300 diff --git a/thirdparty/learning3d/pretrained/exp_pcn/run.log b/thirdparty/learning3d/pretrained/exp_pcn/run.log new file mode 100644 index 0000000000000000000000000000000000000000..8f3e32a699aa25ac1425f5ddd9c82856f2c5d90e --- /dev/null +++ b/thirdparty/learning3d/pretrained/exp_pcn/run.log @@ -0,0 +1,201 @@ +Namespace(batch_size=32, dataset_path='/home/cobra/vinit/pointnetARS/../../ModelNet40/ModelNet40', dataset_type='modelnet', detailed_output=False, device='cuda:0', emb_dims=1024, epochs=200, eval=False, exp_name='exp_pcn', num_points=1024, optimizer='Adam', pretrained='', resume='', seed=1234, start_epoch=0, workers=4) +EPOCH:: 1, Traininig Loss: 0.078061, Testing Loss: 0.059210, Best Loss: 0.059210 +EPOCH:: 2, Traininig Loss: 0.055011, Testing Loss: 0.051732, Best Loss: 0.051732 +EPOCH:: 3, Traininig Loss: 0.049369, Testing Loss: 0.048134, Best Loss: 0.048134 +EPOCH:: 4, Traininig Loss: 0.046692, Testing Loss: 0.045588, Best Loss: 0.045588 +EPOCH:: 5, Traininig Loss: 0.044707, Testing Loss: 0.043828, Best Loss: 0.043828 +EPOCH:: 6, Traininig Loss: 0.043311, Testing Loss: 0.043683, Best Loss: 0.043683 +EPOCH:: 7, Traininig Loss: 0.042120, Testing Loss: 0.042176, Best Loss: 0.042176 +EPOCH:: 8, Traininig Loss: 0.041300, Testing Loss: 0.041397, Best Loss: 0.041397 +EPOCH:: 9, Traininig Loss: 0.040375, Testing Loss: 0.041345, Best Loss: 0.041345 +EPOCH:: 10, Traininig Loss: 0.039835, Testing Loss: 0.041673, Best Loss: 0.041345 +EPOCH:: 11, Traininig Loss: 0.039354, Testing Loss: 0.040279, Best Loss: 0.040279 +EPOCH:: 12, Traininig Loss: 0.038872, Testing Loss: 0.039834, Best Loss: 0.039834 +EPOCH:: 13, Traininig Loss: 0.038594, Testing Loss: 0.040412, Best Loss: 0.039834 +EPOCH:: 14, Traininig Loss: 0.038018, Testing Loss: 0.039285, Best Loss: 0.039285 +EPOCH:: 15, Traininig Loss: 0.037750, Testing Loss: 0.038818, Best Loss: 0.038818 +EPOCH:: 16, Traininig Loss: 0.037272, Testing Loss: 0.040937, Best Loss: 0.038818 +EPOCH:: 17, Traininig Loss: 0.037402, Testing Loss: 0.038583, Best Loss: 0.038583 +EPOCH:: 18, Traininig Loss: 0.036618, Testing Loss: 0.038278, Best Loss: 0.038278 +EPOCH:: 19, Traininig Loss: 0.036699, Testing Loss: 0.038146, Best Loss: 0.038146 +EPOCH:: 20, Traininig Loss: 0.036426, Testing Loss: 0.037728, Best Loss: 0.037728 +EPOCH:: 21, Traininig Loss: 0.035983, Testing Loss: 0.037457, Best Loss: 0.037457 +EPOCH:: 22, Traininig Loss: 0.035916, Testing Loss: 0.037146, Best Loss: 0.037146 +EPOCH:: 23, Traininig Loss: 0.035641, Testing Loss: 0.037431, Best Loss: 0.037146 +EPOCH:: 24, Traininig Loss: 0.035767, Testing Loss: 0.037123, Best Loss: 0.037123 +EPOCH:: 25, Traininig Loss: 0.035273, Testing Loss: 0.037256, Best Loss: 0.037123 +EPOCH:: 26, Traininig Loss: 0.035148, Testing Loss: 0.037105, Best Loss: 0.037105 +EPOCH:: 27, Traininig Loss: 0.035037, Testing Loss: 0.037022, Best Loss: 0.037022 +EPOCH:: 28, Traininig Loss: 0.035063, Testing Loss: 0.037017, Best Loss: 0.037017 +EPOCH:: 29, Traininig Loss: 0.034618, Testing Loss: 0.036711, Best Loss: 0.036711 +EPOCH:: 30, Traininig Loss: 0.034599, Testing Loss: 0.036667, Best Loss: 0.036667 +EPOCH:: 31, Traininig Loss: 0.034514, Testing Loss: 0.036553, Best Loss: 0.036553 +EPOCH:: 32, Traininig Loss: 0.034311, Testing Loss: 0.036619, Best Loss: 0.036553 +EPOCH:: 33, Traininig Loss: 0.034142, Testing Loss: 0.036545, Best Loss: 0.036545 +EPOCH:: 34, Traininig Loss: 0.034254, Testing Loss: 0.036284, Best Loss: 0.036284 +EPOCH:: 35, Traininig Loss: 0.033917, Testing Loss: 0.036394, Best Loss: 0.036284 +EPOCH:: 36, Traininig Loss: 0.033963, Testing Loss: 0.036258, Best Loss: 0.036258 +EPOCH:: 37, Traininig Loss: 0.033840, Testing Loss: 0.037049, Best Loss: 0.036258 +EPOCH:: 38, Traininig Loss: 0.033842, Testing Loss: 0.036184, Best Loss: 0.036184 +EPOCH:: 39, Traininig Loss: 0.033855, Testing Loss: 0.036424, Best Loss: 0.036184 +EPOCH:: 40, Traininig Loss: 0.033470, Testing Loss: 0.035936, Best Loss: 0.035936 +EPOCH:: 41, Traininig Loss: 0.033360, Testing Loss: 0.036403, Best Loss: 0.035936 +EPOCH:: 42, Traininig Loss: 0.033234, Testing Loss: 0.035892, Best Loss: 0.035892 +EPOCH:: 43, Traininig Loss: 0.033268, Testing Loss: 0.035719, Best Loss: 0.035719 +EPOCH:: 44, Traininig Loss: 0.033253, Testing Loss: 0.036050, Best Loss: 0.035719 +EPOCH:: 45, Traininig Loss: 0.033115, Testing Loss: 0.035976, Best Loss: 0.035719 +EPOCH:: 46, Traininig Loss: 0.033207, Testing Loss: 0.035936, Best Loss: 0.035719 +EPOCH:: 47, Traininig Loss: 0.032977, Testing Loss: 0.036099, Best Loss: 0.035719 +EPOCH:: 48, Traininig Loss: 0.032972, Testing Loss: 0.035579, Best Loss: 0.035579 +EPOCH:: 49, Traininig Loss: 0.032807, Testing Loss: 0.035671, Best Loss: 0.035579 +EPOCH:: 50, Traininig Loss: 0.032808, Testing Loss: 0.035796, Best Loss: 0.035579 +EPOCH:: 51, Traininig Loss: 0.032745, Testing Loss: 0.035646, Best Loss: 0.035579 +EPOCH:: 52, Traininig Loss: 0.032692, Testing Loss: 0.035605, Best Loss: 0.035579 +EPOCH:: 53, Traininig Loss: 0.032597, Testing Loss: 0.036273, Best Loss: 0.035579 +EPOCH:: 54, Traininig Loss: 0.032540, Testing Loss: 0.035966, Best Loss: 0.035579 +EPOCH:: 55, Traininig Loss: 0.032488, Testing Loss: 0.035640, Best Loss: 0.035579 +EPOCH:: 56, Traininig Loss: 0.032440, Testing Loss: 0.035593, Best Loss: 0.035579 +EPOCH:: 57, Traininig Loss: 0.032421, Testing Loss: 0.035404, Best Loss: 0.035404 +EPOCH:: 58, Traininig Loss: 0.032181, Testing Loss: 0.035267, Best Loss: 0.035267 +EPOCH:: 59, Traininig Loss: 0.032293, Testing Loss: 0.036148, Best Loss: 0.035267 +EPOCH:: 60, Traininig Loss: 0.032342, Testing Loss: 0.035570, Best Loss: 0.035267 +EPOCH:: 61, Traininig Loss: 0.032185, Testing Loss: 0.035280, Best Loss: 0.035267 +EPOCH:: 62, Traininig Loss: 0.032167, Testing Loss: 0.035475, Best Loss: 0.035267 +EPOCH:: 63, Traininig Loss: 0.032079, Testing Loss: 0.035199, Best Loss: 0.035199 +EPOCH:: 64, Traininig Loss: 0.031953, Testing Loss: 0.035185, Best Loss: 0.035185 +EPOCH:: 65, Traininig Loss: 0.032026, Testing Loss: 0.035413, Best Loss: 0.035185 +EPOCH:: 66, Traininig Loss: 0.032052, Testing Loss: 0.035228, Best Loss: 0.035185 +EPOCH:: 67, Traininig Loss: 0.031812, Testing Loss: 0.035399, Best Loss: 0.035185 +EPOCH:: 68, Traininig Loss: 0.031868, Testing Loss: 0.035564, Best Loss: 0.035185 +EPOCH:: 69, Traininig Loss: 0.031912, Testing Loss: 0.035296, Best Loss: 0.035185 +EPOCH:: 70, Traininig Loss: 0.031771, Testing Loss: 0.035221, Best Loss: 0.035185 +EPOCH:: 71, Traininig Loss: 0.031805, Testing Loss: 0.035736, Best Loss: 0.035185 +EPOCH:: 72, Traininig Loss: 0.031835, Testing Loss: 0.034983, Best Loss: 0.034983 +EPOCH:: 73, Traininig Loss: 0.031835, Testing Loss: 0.035302, Best Loss: 0.034983 +EPOCH:: 74, Traininig Loss: 0.031690, Testing Loss: 0.035209, Best Loss: 0.034983 +EPOCH:: 75, Traininig Loss: 0.031660, Testing Loss: 0.034985, Best Loss: 0.034983 +EPOCH:: 76, Traininig Loss: 0.031569, Testing Loss: 0.035169, Best Loss: 0.034983 +EPOCH:: 77, Traininig Loss: 0.031581, Testing Loss: 0.035064, Best Loss: 0.034983 +EPOCH:: 78, Traininig Loss: 0.031466, Testing Loss: 0.035049, Best Loss: 0.034983 +EPOCH:: 79, Traininig Loss: 0.031505, Testing Loss: 0.035040, Best Loss: 0.034983 +EPOCH:: 80, Traininig Loss: 0.031487, Testing Loss: 0.035282, Best Loss: 0.034983 +EPOCH:: 81, Traininig Loss: 0.031516, Testing Loss: 0.035051, Best Loss: 0.034983 +EPOCH:: 82, Traininig Loss: 0.031428, Testing Loss: 0.034969, Best Loss: 0.034969 +EPOCH:: 83, Traininig Loss: 0.031460, Testing Loss: 0.035052, Best Loss: 0.034969 +EPOCH:: 84, Traininig Loss: 0.031369, Testing Loss: 0.034992, Best Loss: 0.034969 +EPOCH:: 85, Traininig Loss: 0.031243, Testing Loss: 0.035114, Best Loss: 0.034969 +EPOCH:: 86, Traininig Loss: 0.031290, Testing Loss: 0.035043, Best Loss: 0.034969 +EPOCH:: 87, Traininig Loss: 0.031435, Testing Loss: 0.035108, Best Loss: 0.034969 +EPOCH:: 88, Traininig Loss: 0.031243, Testing Loss: 0.035029, Best Loss: 0.034969 +EPOCH:: 89, Traininig Loss: 0.031209, Testing Loss: 0.035367, Best Loss: 0.034969 +EPOCH:: 90, Traininig Loss: 0.031303, Testing Loss: 0.034968, Best Loss: 0.034968 +EPOCH:: 91, Traininig Loss: 0.031145, Testing Loss: 0.034999, Best Loss: 0.034968 +EPOCH:: 92, Traininig Loss: 0.031129, Testing Loss: 0.034860, Best Loss: 0.034860 +EPOCH:: 93, Traininig Loss: 0.030991, Testing Loss: 0.034717, Best Loss: 0.034717 +EPOCH:: 94, Traininig Loss: 0.031262, Testing Loss: 0.035028, Best Loss: 0.034717 +EPOCH:: 95, Traininig Loss: 0.031118, Testing Loss: 0.034897, Best Loss: 0.034717 +EPOCH:: 96, Traininig Loss: 0.031062, Testing Loss: 0.035069, Best Loss: 0.034717 +EPOCH:: 97, Traininig Loss: 0.031032, Testing Loss: 0.035048, Best Loss: 0.034717 +EPOCH:: 98, Traininig Loss: 0.031027, Testing Loss: 0.034905, Best Loss: 0.034717 +EPOCH:: 99, Traininig Loss: 0.031046, Testing Loss: 0.034895, Best Loss: 0.034717 +EPOCH:: 100, Traininig Loss: 0.030916, Testing Loss: 0.035296, Best Loss: 0.034717 +EPOCH:: 101, Traininig Loss: 0.031044, Testing Loss: 0.035043, Best Loss: 0.034717 +EPOCH:: 102, Traininig Loss: 0.030953, Testing Loss: 0.034790, Best Loss: 0.034717 +EPOCH:: 103, Traininig Loss: 0.030794, Testing Loss: 0.034845, Best Loss: 0.034717 +EPOCH:: 104, Traininig Loss: 0.030996, Testing Loss: 0.034961, Best Loss: 0.034717 +EPOCH:: 105, Traininig Loss: 0.030863, Testing Loss: 0.034853, Best Loss: 0.034717 +EPOCH:: 106, Traininig Loss: 0.030810, Testing Loss: 0.035015, Best Loss: 0.034717 +EPOCH:: 107, Traininig Loss: 0.030881, Testing Loss: 0.034883, Best Loss: 0.034717 +EPOCH:: 108, Traininig Loss: 0.030897, Testing Loss: 0.034896, Best Loss: 0.034717 +EPOCH:: 109, Traininig Loss: 0.030785, Testing Loss: 0.034938, Best Loss: 0.034717 +EPOCH:: 110, Traininig Loss: 0.030788, Testing Loss: 0.035058, Best Loss: 0.034717 +EPOCH:: 111, Traininig Loss: 0.030751, Testing Loss: 0.035018, Best Loss: 0.034717 +EPOCH:: 112, Traininig Loss: 0.030797, Testing Loss: 0.034764, Best Loss: 0.034717 +EPOCH:: 113, Traininig Loss: 0.030838, Testing Loss: 0.034981, Best Loss: 0.034717 +EPOCH:: 114, Traininig Loss: 0.030763, Testing Loss: 0.034846, Best Loss: 0.034717 +EPOCH:: 115, Traininig Loss: 0.030798, Testing Loss: 0.034748, Best Loss: 0.034717 +EPOCH:: 116, Traininig Loss: 0.030644, Testing Loss: 0.035099, Best Loss: 0.034717 +EPOCH:: 117, Traininig Loss: 0.030681, Testing Loss: 0.034804, Best Loss: 0.034717 +EPOCH:: 118, Traininig Loss: 0.030646, Testing Loss: 0.034936, Best Loss: 0.034717 +EPOCH:: 119, Traininig Loss: 0.030796, Testing Loss: 0.034904, Best Loss: 0.034717 +EPOCH:: 120, Traininig Loss: 0.030637, Testing Loss: 0.034887, Best Loss: 0.034717 +EPOCH:: 121, Traininig Loss: 0.030601, Testing Loss: 0.034736, Best Loss: 0.034717 +EPOCH:: 122, Traininig Loss: 0.030623, Testing Loss: 0.034860, Best Loss: 0.034717 +EPOCH:: 123, Traininig Loss: 0.030627, Testing Loss: 0.034824, Best Loss: 0.034717 +EPOCH:: 124, Traininig Loss: 0.030546, Testing Loss: 0.035058, Best Loss: 0.034717 +EPOCH:: 125, Traininig Loss: 0.030616, Testing Loss: 0.034811, Best Loss: 0.034717 +EPOCH:: 126, Traininig Loss: 0.030665, Testing Loss: 0.034820, Best Loss: 0.034717 +EPOCH:: 127, Traininig Loss: 0.030522, Testing Loss: 0.034907, Best Loss: 0.034717 +EPOCH:: 128, Traininig Loss: 0.030558, Testing Loss: 0.034762, Best Loss: 0.034717 +EPOCH:: 129, Traininig Loss: 0.030520, Testing Loss: 0.034868, Best Loss: 0.034717 +EPOCH:: 130, Traininig Loss: 0.030480, Testing Loss: 0.034709, Best Loss: 0.034709 +EPOCH:: 131, Traininig Loss: 0.030503, Testing Loss: 0.034891, Best Loss: 0.034709 +EPOCH:: 132, Traininig Loss: 0.030473, Testing Loss: 0.034836, Best Loss: 0.034709 +EPOCH:: 133, Traininig Loss: 0.030535, Testing Loss: 0.034651, Best Loss: 0.034651 +EPOCH:: 134, Traininig Loss: 0.030407, Testing Loss: 0.034746, Best Loss: 0.034651 +EPOCH:: 135, Traininig Loss: 0.030405, Testing Loss: 0.034769, Best Loss: 0.034651 +EPOCH:: 136, Traininig Loss: 0.030428, Testing Loss: 0.034659, Best Loss: 0.034651 +EPOCH:: 137, Traininig Loss: 0.030418, Testing Loss: 0.034718, Best Loss: 0.034651 +EPOCH:: 138, Traininig Loss: 0.030436, Testing Loss: 0.034754, Best Loss: 0.034651 +EPOCH:: 139, Traininig Loss: 0.030416, Testing Loss: 0.034661, Best Loss: 0.034651 +EPOCH:: 140, Traininig Loss: 0.030367, Testing Loss: 0.034768, Best Loss: 0.034651 +EPOCH:: 141, Traininig Loss: 0.030393, Testing Loss: 0.034834, Best Loss: 0.034651 +EPOCH:: 142, Traininig Loss: 0.030248, Testing Loss: 0.034719, Best Loss: 0.034651 +EPOCH:: 143, Traininig Loss: 0.030324, Testing Loss: 0.034855, Best Loss: 0.034651 +EPOCH:: 144, Traininig Loss: 0.030363, Testing Loss: 0.034934, Best Loss: 0.034651 +EPOCH:: 145, Traininig Loss: 0.030341, Testing Loss: 0.034824, Best Loss: 0.034651 +EPOCH:: 146, Traininig Loss: 0.030315, Testing Loss: 0.034692, Best Loss: 0.034651 +EPOCH:: 147, Traininig Loss: 0.030321, Testing Loss: 0.034742, Best Loss: 0.034651 +EPOCH:: 148, Traininig Loss: 0.030337, Testing Loss: 0.034849, Best Loss: 0.034651 +EPOCH:: 149, Traininig Loss: 0.030310, Testing Loss: 0.034786, Best Loss: 0.034651 +EPOCH:: 150, Traininig Loss: 0.030279, Testing Loss: 0.034738, Best Loss: 0.034651 +EPOCH:: 151, Traininig Loss: 0.030268, Testing Loss: 0.034732, Best Loss: 0.034651 +EPOCH:: 152, Traininig Loss: 0.030237, Testing Loss: 0.034756, Best Loss: 0.034651 +EPOCH:: 153, Traininig Loss: 0.030282, Testing Loss: 0.035022, Best Loss: 0.034651 +EPOCH:: 154, Traininig Loss: 0.030347, Testing Loss: 0.034753, Best Loss: 0.034651 +EPOCH:: 155, Traininig Loss: 0.030207, Testing Loss: 0.034953, Best Loss: 0.034651 +EPOCH:: 156, Traininig Loss: 0.030257, Testing Loss: 0.034715, Best Loss: 0.034651 +EPOCH:: 157, Traininig Loss: 0.030159, Testing Loss: 0.034642, Best Loss: 0.034642 +EPOCH:: 158, Traininig Loss: 0.030121, Testing Loss: 0.034725, Best Loss: 0.034642 +EPOCH:: 159, Traininig Loss: 0.030200, Testing Loss: 0.034766, Best Loss: 0.034642 +EPOCH:: 160, Traininig Loss: 0.030142, Testing Loss: 0.034667, Best Loss: 0.034642 +EPOCH:: 161, Traininig Loss: 0.030192, Testing Loss: 0.034949, Best Loss: 0.034642 +EPOCH:: 162, Traininig Loss: 0.030192, Testing Loss: 0.035057, Best Loss: 0.034642 +EPOCH:: 163, Traininig Loss: 0.030292, Testing Loss: 0.034988, Best Loss: 0.034642 +EPOCH:: 164, Traininig Loss: 0.030205, Testing Loss: 0.034578, Best Loss: 0.034578 +EPOCH:: 165, Traininig Loss: 0.030043, Testing Loss: 0.034701, Best Loss: 0.034578 +EPOCH:: 166, Traininig Loss: 0.030106, Testing Loss: 0.034733, Best Loss: 0.034578 +EPOCH:: 167, Traininig Loss: 0.030087, Testing Loss: 0.034808, Best Loss: 0.034578 +EPOCH:: 168, Traininig Loss: 0.030113, Testing Loss: 0.034851, Best Loss: 0.034578 +EPOCH:: 169, Traininig Loss: 0.030068, Testing Loss: 0.034745, Best Loss: 0.034578 +EPOCH:: 170, Traininig Loss: 0.030111, Testing Loss: 0.035037, Best Loss: 0.034578 +EPOCH:: 171, Traininig Loss: 0.030105, Testing Loss: 0.034678, Best Loss: 0.034578 +EPOCH:: 172, Traininig Loss: 0.030064, Testing Loss: 0.035076, Best Loss: 0.034578 +EPOCH:: 173, Traininig Loss: 0.030094, Testing Loss: 0.034748, Best Loss: 0.034578 +EPOCH:: 174, Traininig Loss: 0.030116, Testing Loss: 0.034720, Best Loss: 0.034578 +EPOCH:: 175, Traininig Loss: 0.030172, Testing Loss: 0.034672, Best Loss: 0.034578 +EPOCH:: 176, Traininig Loss: 0.030137, Testing Loss: 0.034858, Best Loss: 0.034578 +EPOCH:: 177, Traininig Loss: 0.029986, Testing Loss: 0.034676, Best Loss: 0.034578 +EPOCH:: 178, Traininig Loss: 0.030018, Testing Loss: 0.034811, Best Loss: 0.034578 +EPOCH:: 179, Traininig Loss: 0.030068, Testing Loss: 0.034774, Best Loss: 0.034578 +EPOCH:: 180, Traininig Loss: 0.030044, Testing Loss: 0.034773, Best Loss: 0.034578 +EPOCH:: 181, Traininig Loss: 0.029916, Testing Loss: 0.034641, Best Loss: 0.034578 +EPOCH:: 182, Traininig Loss: 0.029918, Testing Loss: 0.034606, Best Loss: 0.034578 +EPOCH:: 183, Traininig Loss: 0.030029, Testing Loss: 0.034599, Best Loss: 0.034578 +EPOCH:: 184, Traininig Loss: 0.029996, Testing Loss: 0.034719, Best Loss: 0.034578 +EPOCH:: 185, Traininig Loss: 0.029963, Testing Loss: 0.034738, Best Loss: 0.034578 +EPOCH:: 186, Traininig Loss: 0.029990, Testing Loss: 0.034830, Best Loss: 0.034578 +EPOCH:: 187, Traininig Loss: 0.030041, Testing Loss: 0.034790, Best Loss: 0.034578 +EPOCH:: 188, Traininig Loss: 0.029984, Testing Loss: 0.035008, Best Loss: 0.034578 +EPOCH:: 189, Traininig Loss: 0.030129, Testing Loss: 0.034721, Best Loss: 0.034578 +EPOCH:: 190, Traininig Loss: 0.029938, Testing Loss: 0.034797, Best Loss: 0.034578 +EPOCH:: 191, Traininig Loss: 0.029848, Testing Loss: 0.034815, Best Loss: 0.034578 +EPOCH:: 192, Traininig Loss: 0.029863, Testing Loss: 0.034699, Best Loss: 0.034578 +EPOCH:: 193, Traininig Loss: 0.029903, Testing Loss: 0.034896, Best Loss: 0.034578 +EPOCH:: 194, Traininig Loss: 0.030034, Testing Loss: 0.034703, Best Loss: 0.034578 +EPOCH:: 195, Traininig Loss: 0.029895, Testing Loss: 0.034606, Best Loss: 0.034578 +EPOCH:: 196, Traininig Loss: 0.029930, Testing Loss: 0.034706, Best Loss: 0.034578 +EPOCH:: 197, Traininig Loss: 0.029950, Testing Loss: 0.034865, Best Loss: 0.034578 +EPOCH:: 198, Traininig Loss: 0.029993, Testing Loss: 0.034775, Best Loss: 0.034578 +EPOCH:: 199, Traininig Loss: 0.029904, Testing Loss: 0.034727, Best Loss: 0.034578 +EPOCH:: 200, Traininig Loss: 0.029912, Testing Loss: 0.034583, Best Loss: 0.034578 diff --git a/thirdparty/learning3d/pretrained/exp_pnlk/events.out.tfevents.1584591855.bioroboticslab b/thirdparty/learning3d/pretrained/exp_pnlk/events.out.tfevents.1584591855.bioroboticslab new file mode 100644 index 0000000000000000000000000000000000000000..a6a44688e3c69fda686a1843ebcaf813c8bff102 --- /dev/null +++ b/thirdparty/learning3d/pretrained/exp_pnlk/events.out.tfevents.1584591855.bioroboticslab @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:deb7d4457a03bc5cabcf8d0006739fe1ece3acfac990a5964f07553c3063e829 +size 21859 diff --git a/thirdparty/learning3d/pretrained/exp_pnlk/models/best_model.t7 b/thirdparty/learning3d/pretrained/exp_pnlk/models/best_model.t7 new file mode 100644 index 0000000000000000000000000000000000000000..7215ebcacc345e7dbeacf64c04b7111db341785b --- /dev/null +++ b/thirdparty/learning3d/pretrained/exp_pnlk/models/best_model.t7 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:27549313110416815fc9b1776983b80406e4d4572d57f67f1a6f3ede0745481d +size 623082 diff --git a/thirdparty/learning3d/pretrained/exp_pnlk/models/best_model_snap.t7 b/thirdparty/learning3d/pretrained/exp_pnlk/models/best_model_snap.t7 new file mode 100644 index 0000000000000000000000000000000000000000..74c40f29506b1b54587819ae73423641aea28a68 --- /dev/null +++ b/thirdparty/learning3d/pretrained/exp_pnlk/models/best_model_snap.t7 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:182bfc8ee69670197c81fa90b807bfc28eebdfbefe89bca30831b0acdeb738fe +size 1842538 diff --git a/thirdparty/learning3d/pretrained/exp_pnlk/models/best_ptnet_model.t7 b/thirdparty/learning3d/pretrained/exp_pnlk/models/best_ptnet_model.t7 new file mode 100644 index 0000000000000000000000000000000000000000..53412384c0aee20ce5859ba5c39fa854a952f4b0 --- /dev/null +++ b/thirdparty/learning3d/pretrained/exp_pnlk/models/best_ptnet_model.t7 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:03923dc965fc75c3df67ffdd7f868e6822083ea22d893c4f8929474c9cebb52e +size 622229 diff --git a/thirdparty/learning3d/pretrained/exp_pnlk/run.log b/thirdparty/learning3d/pretrained/exp_pnlk/run.log new file mode 100644 index 0000000000000000000000000000000000000000..97caf0a72d6c9aa42f727137f5dc86adcffc0a63 --- /dev/null +++ b/thirdparty/learning3d/pretrained/exp_pnlk/run.log @@ -0,0 +1,149 @@ +Namespace(batch_size=10, dataset_path='ModelNet40', dataset_type='modelnet', device='cuda:0', emb_dims=1024, epochs=200, eval=False, exp_name='exp_pnlk_v1', fine_tune_pointnet='tune', num_points=1024, optimizer='Adam', pretrained='', resume='', seed=1234, start_epoch=0, symfn='max', transfer_ptnet_weights='./checkpoints/exp_classifier/models/best_ptnet_model.t7', workers=4) +EPOCH:: 1, Traininig Loss: 2.834779, Testing Loss: 2.354854, Best Loss: 2.354854 +EPOCH:: 2, Traininig Loss: 1.064775, Testing Loss: 1.424999, Best Loss: 1.424999 +EPOCH:: 3, Traininig Loss: 0.665383, Testing Loss: 0.739468, Best Loss: 0.739468 +EPOCH:: 4, Traininig Loss: 0.855117, Testing Loss: 0.664866, Best Loss: 0.664866 +EPOCH:: 5, Traininig Loss: 3.864886, Testing Loss: 0.336409, Best Loss: 0.336409 +EPOCH:: 6, Traininig Loss: 0.780387, Testing Loss: 0.402455, Best Loss: 0.336409 +EPOCH:: 7, Traininig Loss: 0.504956, Testing Loss: 0.326164, Best Loss: 0.326164 +EPOCH:: 8, Traininig Loss: 0.919040, Testing Loss: 0.827797, Best Loss: 0.326164 +EPOCH:: 9, Traininig Loss: 0.678414, Testing Loss: 0.956458, Best Loss: 0.326164 +EPOCH:: 10, Traininig Loss: 1.179589, Testing Loss: 1.247572, Best Loss: 0.326164 +EPOCH:: 11, Traininig Loss: 0.649622, Testing Loss: 0.723166, Best Loss: 0.326164 +EPOCH:: 12, Traininig Loss: 0.994429, Testing Loss: 0.375436, Best Loss: 0.326164 +EPOCH:: 13, Traininig Loss: 0.821025, Testing Loss: 0.694881, Best Loss: 0.326164 +EPOCH:: 14, Traininig Loss: 1.112250, Testing Loss: 2.485548, Best Loss: 0.326164 +EPOCH:: 15, Traininig Loss: 2.370769, Testing Loss: 2.375044, Best Loss: 0.326164 +EPOCH:: 16, Traininig Loss: 1.054083, Testing Loss: 0.626288, Best Loss: 0.326164 +EPOCH:: 17, Traininig Loss: 0.783341, Testing Loss: 0.839274, Best Loss: 0.326164 +EPOCH:: 18, Traininig Loss: 0.628790, Testing Loss: 0.268407, Best Loss: 0.268407 +EPOCH:: 19, Traininig Loss: 0.671890, Testing Loss: 0.323315, Best Loss: 0.268407 +EPOCH:: 20, Traininig Loss: 0.678178, Testing Loss: 1.369686, Best Loss: 0.268407 +EPOCH:: 21, Traininig Loss: 0.639423, Testing Loss: 0.336060, Best Loss: 0.268407 +EPOCH:: 22, Traininig Loss: 1.266182, Testing Loss: 0.349840, Best Loss: 0.268407 +EPOCH:: 23, Traininig Loss: 0.467277, Testing Loss: 0.541252, Best Loss: 0.268407 +EPOCH:: 24, Traininig Loss: 0.686518, Testing Loss: 0.411531, Best Loss: 0.268407 +EPOCH:: 25, Traininig Loss: 0.432976, Testing Loss: 0.339188, Best Loss: 0.268407 +EPOCH:: 26, Traininig Loss: 0.356039, Testing Loss: 0.327641, Best Loss: 0.268407 +EPOCH:: 27, Traininig Loss: 0.918736, Testing Loss: 1.660314, Best Loss: 0.268407 +EPOCH:: 28, Traininig Loss: 0.480436, Testing Loss: 0.286674, Best Loss: 0.268407 +EPOCH:: 29, Traininig Loss: 0.415409, Testing Loss: 0.989688, Best Loss: 0.268407 +EPOCH:: 30, Traininig Loss: 0.913350, Testing Loss: 1.347495, Best Loss: 0.268407 +EPOCH:: 31, Traininig Loss: 1.057996, Testing Loss: 1.023880, Best Loss: 0.268407 +EPOCH:: 32, Traininig Loss: 0.441287, Testing Loss: 0.470999, Best Loss: 0.268407 +EPOCH:: 33, Traininig Loss: 0.579684, Testing Loss: 0.221836, Best Loss: 0.221836 +EPOCH:: 34, Traininig Loss: 0.454712, Testing Loss: 0.280715, Best Loss: 0.221836 +EPOCH:: 35, Traininig Loss: 0.798291, Testing Loss: 0.372970, Best Loss: 0.221836 +EPOCH:: 36, Traininig Loss: 0.359684, Testing Loss: 0.306898, Best Loss: 0.221836 +EPOCH:: 37, Traininig Loss: 1.022783, Testing Loss: 0.271976, Best Loss: 0.221836 +EPOCH:: 38, Traininig Loss: 0.693582, Testing Loss: 0.172906, Best Loss: 0.172906 +EPOCH:: 39, Traininig Loss: 0.341462, Testing Loss: 0.701160, Best Loss: 0.172906 +EPOCH:: 40, Traininig Loss: 0.342833, Testing Loss: 0.227766, Best Loss: 0.172906 +EPOCH:: 41, Traininig Loss: 0.365303, Testing Loss: 0.207183, Best Loss: 0.172906 +EPOCH:: 42, Traininig Loss: 0.393086, Testing Loss: 0.646474, Best Loss: 0.172906 +EPOCH:: 43, Traininig Loss: 0.496237, Testing Loss: 0.273979, Best Loss: 0.172906 +EPOCH:: 44, Traininig Loss: 0.523359, Testing Loss: 0.368619, Best Loss: 0.172906 +EPOCH:: 45, Traininig Loss: 0.532088, Testing Loss: 0.183789, Best Loss: 0.172906 +EPOCH:: 46, Traininig Loss: 0.472344, Testing Loss: 0.236377, Best Loss: 0.172906 +EPOCH:: 47, Traininig Loss: 0.494629, Testing Loss: 0.483768, Best Loss: 0.172906 +EPOCH:: 48, Traininig Loss: 0.501748, Testing Loss: 0.357958, Best Loss: 0.172906 +EPOCH:: 49, Traininig Loss: 0.396413, Testing Loss: 0.282505, Best Loss: 0.172906 +EPOCH:: 50, Traininig Loss: 0.623621, Testing Loss: 0.669350, Best Loss: 0.172906 +EPOCH:: 51, Traininig Loss: 0.446295, Testing Loss: 0.288089, Best Loss: 0.172906 +EPOCH:: 52, Traininig Loss: 0.399694, Testing Loss: 0.489038, Best Loss: 0.172906 +EPOCH:: 53, Traininig Loss: 0.400650, Testing Loss: 0.342359, Best Loss: 0.172906 +EPOCH:: 54, Traininig Loss: 0.422242, Testing Loss: 0.207871, Best Loss: 0.172906 +EPOCH:: 55, Traininig Loss: 0.283609, Testing Loss: 0.448490, Best Loss: 0.172906 +EPOCH:: 56, Traininig Loss: 0.329588, Testing Loss: 0.161190, Best Loss: 0.161190 +EPOCH:: 57, Traininig Loss: 0.263220, Testing Loss: 0.153100, Best Loss: 0.153100 +EPOCH:: 58, Traininig Loss: 0.393380, Testing Loss: 0.152248, Best Loss: 0.152248 +EPOCH:: 59, Traininig Loss: 0.478696, Testing Loss: 0.313889, Best Loss: 0.152248 +EPOCH:: 60, Traininig Loss: 0.285337, Testing Loss: 0.248272, Best Loss: 0.152248 +EPOCH:: 61, Traininig Loss: 0.266053, Testing Loss: 0.464098, Best Loss: 0.152248 +EPOCH:: 62, Traininig Loss: 0.456450, Testing Loss: 0.419472, Best Loss: 0.152248 +EPOCH:: 63, Traininig Loss: 0.268725, Testing Loss: 0.913477, Best Loss: 0.152248 +EPOCH:: 64, Traininig Loss: 0.524866, Testing Loss: 0.848576, Best Loss: 0.152248 +EPOCH:: 65, Traininig Loss: 0.374749, Testing Loss: 0.406261, Best Loss: 0.152248 +EPOCH:: 66, Traininig Loss: 0.436233, Testing Loss: 0.188461, Best Loss: 0.152248 +EPOCH:: 67, Traininig Loss: 0.334132, Testing Loss: 0.340776, Best Loss: 0.152248 +EPOCH:: 68, Traininig Loss: 0.387510, Testing Loss: 0.197675, Best Loss: 0.152248 +EPOCH:: 69, Traininig Loss: 0.541314, Testing Loss: 0.327022, Best Loss: 0.152248 +EPOCH:: 70, Traininig Loss: 0.474901, Testing Loss: 0.157972, Best Loss: 0.152248 +EPOCH:: 71, Traininig Loss: 0.594644, Testing Loss: 0.395266, Best Loss: 0.152248 +EPOCH:: 72, Traininig Loss: 0.346124, Testing Loss: 0.140594, Best Loss: 0.140594 +EPOCH:: 73, Traininig Loss: 0.440301, Testing Loss: 0.224476, Best Loss: 0.140594 +EPOCH:: 74, Traininig Loss: 0.293946, Testing Loss: 0.309833, Best Loss: 0.140594 +EPOCH:: 75, Traininig Loss: 0.276344, Testing Loss: 0.170317, Best Loss: 0.140594 +EPOCH:: 76, Traininig Loss: 0.266229, Testing Loss: 0.381933, Best Loss: 0.140594 +EPOCH:: 77, Traininig Loss: 0.279818, Testing Loss: 0.164500, Best Loss: 0.140594 +EPOCH:: 78, Traininig Loss: 0.276376, Testing Loss: 0.302418, Best Loss: 0.140594 +EPOCH:: 79, Traininig Loss: 0.252992, Testing Loss: 0.210965, Best Loss: 0.140594 +EPOCH:: 80, Traininig Loss: 0.273038, Testing Loss: 0.411084, Best Loss: 0.140594 +EPOCH:: 81, Traininig Loss: 0.290430, Testing Loss: 0.232634, Best Loss: 0.140594 +EPOCH:: 82, Traininig Loss: 0.168238, Testing Loss: 0.139678, Best Loss: 0.139678 +EPOCH:: 83, Traininig Loss: 0.201684, Testing Loss: 0.176658, Best Loss: 0.139678 +EPOCH:: 84, Traininig Loss: 0.323821, Testing Loss: 0.158654, Best Loss: 0.139678 +EPOCH:: 85, Traininig Loss: 0.276069, Testing Loss: 0.539330, Best Loss: 0.139678 +EPOCH:: 86, Traininig Loss: 0.296292, Testing Loss: 0.353222, Best Loss: 0.139678 +EPOCH:: 87, Traininig Loss: 0.290456, Testing Loss: 0.201234, Best Loss: 0.139678 +EPOCH:: 88, Traininig Loss: 0.194857, Testing Loss: 0.301388, Best Loss: 0.139678 +EPOCH:: 89, Traininig Loss: 0.175779, Testing Loss: 0.258349, Best Loss: 0.139678 +EPOCH:: 90, Traininig Loss: 0.409617, Testing Loss: 0.175406, Best Loss: 0.139678 +EPOCH:: 91, Traininig Loss: 0.582497, Testing Loss: 0.254861, Best Loss: 0.139678 +EPOCH:: 92, Traininig Loss: 0.240898, Testing Loss: 0.242189, Best Loss: 0.139678 +EPOCH:: 93, Traininig Loss: 0.257769, Testing Loss: 0.315680, Best Loss: 0.139678 +EPOCH:: 94, Traininig Loss: 0.230241, Testing Loss: 0.153513, Best Loss: 0.139678 +EPOCH:: 95, Traininig Loss: 0.332786, Testing Loss: 0.427346, Best Loss: 0.139678 +EPOCH:: 96, Traininig Loss: 0.289687, Testing Loss: 0.388985, Best Loss: 0.139678 +EPOCH:: 97, Traininig Loss: 0.220344, Testing Loss: 0.147901, Best Loss: 0.139678 +EPOCH:: 98, Traininig Loss: 0.231428, Testing Loss: 0.149355, Best Loss: 0.139678 +EPOCH:: 99, Traininig Loss: 0.178413, Testing Loss: 0.148281, Best Loss: 0.139678 +EPOCH:: 100, Traininig Loss: 0.309711, Testing Loss: 0.276784, Best Loss: 0.139678 +EPOCH:: 101, Traininig Loss: 0.377363, Testing Loss: 0.222645, Best Loss: 0.139678 +EPOCH:: 102, Traininig Loss: 0.250462, Testing Loss: 0.134726, Best Loss: 0.134726 +EPOCH:: 103, Traininig Loss: 0.405897, Testing Loss: 0.275517, Best Loss: 0.134726 +EPOCH:: 104, Traininig Loss: 0.331990, Testing Loss: 0.234569, Best Loss: 0.134726 +EPOCH:: 105, Traininig Loss: 0.254683, Testing Loss: 0.175339, Best Loss: 0.134726 +EPOCH:: 106, Traininig Loss: 0.619422, Testing Loss: 0.540907, Best Loss: 0.134726 +EPOCH:: 107, Traininig Loss: 0.705300, Testing Loss: 0.313855, Best Loss: 0.134726 +EPOCH:: 108, Traininig Loss: 0.331038, Testing Loss: 0.151469, Best Loss: 0.134726 +EPOCH:: 109, Traininig Loss: 0.390007, Testing Loss: 0.150629, Best Loss: 0.134726 +EPOCH:: 110, Traininig Loss: 0.226978, Testing Loss: 0.251935, Best Loss: 0.134726 +EPOCH:: 111, Traininig Loss: 0.226525, Testing Loss: 0.178066, Best Loss: 0.134726 +EPOCH:: 112, Traininig Loss: 0.218872, Testing Loss: 0.170914, Best Loss: 0.134726 +EPOCH:: 113, Traininig Loss: 0.201866, Testing Loss: 0.169647, Best Loss: 0.134726 +EPOCH:: 114, Traininig Loss: 0.209271, Testing Loss: 0.143323, Best Loss: 0.134726 +EPOCH:: 115, Traininig Loss: 0.280734, Testing Loss: 0.232828, Best Loss: 0.134726 +EPOCH:: 116, Traininig Loss: 0.254841, Testing Loss: 0.226017, Best Loss: 0.134726 +EPOCH:: 117, Traininig Loss: 0.975226, Testing Loss: 0.837092, Best Loss: 0.134726 +EPOCH:: 118, Traininig Loss: 1.816730, Testing Loss: 2.717862, Best Loss: 0.134726 +EPOCH:: 119, Traininig Loss: 2.842766, Testing Loss: 0.485672, Best Loss: 0.134726 +EPOCH:: 120, Traininig Loss: 0.845311, Testing Loss: 0.668476, Best Loss: 0.134726 +EPOCH:: 121, Traininig Loss: 0.460078, Testing Loss: 0.273603, Best Loss: 0.134726 +EPOCH:: 122, Traininig Loss: 1.078342, Testing Loss: 0.540457, Best Loss: 0.134726 +EPOCH:: 123, Traininig Loss: 0.483985, Testing Loss: 0.530448, Best Loss: 0.134726 +EPOCH:: 124, Traininig Loss: 4.263491, Testing Loss: 0.346515, Best Loss: 0.134726 +EPOCH:: 125, Traininig Loss: 0.355456, Testing Loss: 0.224295, Best Loss: 0.134726 +EPOCH:: 126, Traininig Loss: 0.498781, Testing Loss: 0.331865, Best Loss: 0.134726 +EPOCH:: 127, Traininig Loss: 0.373729, Testing Loss: 0.177964, Best Loss: 0.134726 +EPOCH:: 128, Traininig Loss: 0.347582, Testing Loss: 0.397925, Best Loss: 0.134726 +EPOCH:: 129, Traininig Loss: 0.385803, Testing Loss: 0.287190, Best Loss: 0.134726 +EPOCH:: 130, Traininig Loss: 0.349369, Testing Loss: 0.180405, Best Loss: 0.134726 +EPOCH:: 131, Traininig Loss: 0.321447, Testing Loss: 0.243773, Best Loss: 0.134726 +EPOCH:: 132, Traininig Loss: 0.309076, Testing Loss: 0.282705, Best Loss: 0.134726 +EPOCH:: 133, Traininig Loss: 0.285327, Testing Loss: 0.202457, Best Loss: 0.134726 +EPOCH:: 134, Traininig Loss: 0.344632, Testing Loss: 0.164050, Best Loss: 0.134726 +EPOCH:: 135, Traininig Loss: 0.479969, Testing Loss: 0.512099, Best Loss: 0.134726 +EPOCH:: 136, Traininig Loss: 0.824751, Testing Loss: 0.474732, Best Loss: 0.134726 +EPOCH:: 137, Traininig Loss: 0.646979, Testing Loss: 0.337876, Best Loss: 0.134726 +EPOCH:: 138, Traininig Loss: 0.915418, Testing Loss: 0.591881, Best Loss: 0.134726 +EPOCH:: 139, Traininig Loss: 0.672524, Testing Loss: 0.501628, Best Loss: 0.134726 +EPOCH:: 140, Traininig Loss: 0.468465, Testing Loss: 0.463384, Best Loss: 0.134726 +EPOCH:: 141, Traininig Loss: 0.486230, Testing Loss: 0.404106, Best Loss: 0.134726 +EPOCH:: 142, Traininig Loss: 1.036846, Testing Loss: 0.664894, Best Loss: 0.134726 +EPOCH:: 143, Traininig Loss: 0.699049, Testing Loss: 0.591680, Best Loss: 0.134726 +EPOCH:: 144, Traininig Loss: 0.761399, Testing Loss: 0.628964, Best Loss: 0.134726 +EPOCH:: 145, Traininig Loss: 0.537192, Testing Loss: 0.524663, Best Loss: 0.134726 +EPOCH:: 146, Traininig Loss: 0.373876, Testing Loss: 0.297446, Best Loss: 0.134726 +EPOCH:: 147, Traininig Loss: 0.318644, Testing Loss: 0.249449, Best Loss: 0.134726 +EPOCH:: 148, Traininig Loss: 0.312249, Testing Loss: 0.202475, Best Loss: 0.134726 diff --git a/thirdparty/learning3d/pretrained/exp_prnet/args.txt b/thirdparty/learning3d/pretrained/exp_prnet/args.txt new file mode 100644 index 0000000000000000000000000000000000000000..6ed07648ecd8e9f9bb30250644920960b61d08f2 --- /dev/null +++ b/thirdparty/learning3d/pretrained/exp_prnet/args.txt @@ -0,0 +1,35 @@ +{ + "exp_name": "exp_multi_gpu_v1", + "model": "prnet", + "emb_nn": "dgcnn", + "attention": "transformer", + "head": "svd", + "n_emb_dims": 512, + "n_blocks": 1, + "n_heads": 4, + "n_iters": 3, + "discount_factor": 0.9, + "n_ff_dims": 1024, + "n_keypoints": 512, + "temp_factor": 100, + "cat_sampler": "softmax", + "dropout": 0.0, + "batch_size": 16, + "test_batch_size": 8, + "epochs": 100, + "use_sgd": false, + "lr": 0.001, + "momentum": 0.9, + "no_cuda": false, + "seed": 1234, + "eval": false, + "cycle_consistency_loss": 0.1, + "feature_alignment_loss": 0.1, + "gaussian_noise": false, + "unseen": false, + "n_points": 1024, + "n_subsampled_points": 768, + "dataset": "modelnet40", + "rot_factor": 4, + "model_path": "" +} \ No newline at end of file diff --git a/thirdparty/learning3d/pretrained/exp_prnet/log b/thirdparty/learning3d/pretrained/exp_prnet/log new file mode 100644 index 0000000000000000000000000000000000000000..36aefe6105693ccba0bf7724e07baf1eb6c66f35 --- /dev/null +++ b/thirdparty/learning3d/pretrained/exp_prnet/log @@ -0,0 +1,302 @@ +Namespace(attention='transformer', batch_size=16, cat_sampler='softmax', cycle_consistency_loss=0.1, dataset='modelnet40', discount_factor=0.9, dropout=0.0, emb_nn='dgcnn', epochs=100, eval=False, exp_name='exp_multi_gpu_v1', feature_alignment_loss=0.1, gaussian_noise=False, head='svd', lr=0.001, model='prnet', model_path='', momentum=0.9, n_blocks=1, n_emb_dims=512, n_ff_dims=1024, n_heads=4, n_iters=3, n_keypoints=512, n_points=1024, n_subsampled_points=768, no_cuda=False, rot_factor=4, seed=1234, temp_factor=100, test_batch_size=8, unseen=False, use_sgd=False) +Namespace(attention='transformer', batch_size=16, cat_sampler='softmax', cycle_consistency_loss=0.1, dataset='modelnet40', discount_factor=0.9, dropout=0.0, emb_nn='dgcnn', epochs=100, eval=False, exp_name='exp_multi_gpu_v1', feature_alignment_loss=0.1, gaussian_noise=False, head='svd', lr=0.001, model='prnet', model_path='', momentum=0.9, n_blocks=1, n_emb_dims=512, n_ff_dims=1024, n_heads=4, n_iters=3, n_keypoints=512, n_points=1024, n_subsampled_points=768, no_cuda=False, rot_factor=4, seed=1234, temp_factor=100, test_batch_size=8, unseen=False, use_sgd=False) +A->B:: Stage: train, Epoch: 0, Loss: 0.215356, Feature_alignment_loss: 0.032233, Cycle_consistency_loss: 0.004296, Scale_consensus_loss: 0.000000, Rot_MSE: 269.768463, Rot_RMSE: 16.424629, Rot_MAE: 10.563557, Rot_R2: -0.601557, Trans_MSE: 0.022195, Trans_RMSE: 0.148979, Trans_MAE: 0.105661, Trans_R2: 0.732826 +A->B:: Stage: test, Epoch: 0, Loss: 0.166565, Feature_alignment_loss: 0.025816, Cycle_consistency_loss: 0.003925, Scale_consensus_loss: 0.000000, Rot_MSE: 179.643295, Rot_RMSE: 13.403108, Rot_MAE: 9.078445, Rot_R2: -0.065260, Trans_MSE: 0.015727, Trans_RMSE: 0.125406, Trans_MAE: 0.087686, Trans_R2: 0.809000 +A->B:: Stage: best_test, Epoch: 0, Loss: 0.166565, Feature_alignment_loss: 0.025816, Cycle_consistency_loss: 0.003925, Scale_consensus_loss: 0.000000, Rot_MSE: 179.643295, Rot_RMSE: 13.403108, Rot_MAE: 9.078445, Rot_R2: -0.065260, Trans_MSE: 0.015727, Trans_RMSE: 0.125406, Trans_MAE: 0.087686, Trans_R2: 0.809000 +A->B:: Stage: train, Epoch: 1, Loss: 0.156060, Feature_alignment_loss: 0.019597, Cycle_consistency_loss: 0.003541, Scale_consensus_loss: 0.000000, Rot_MSE: 191.985962, Rot_RMSE: 13.855900, Rot_MAE: 9.163855, Rot_R2: -0.139860, Trans_MSE: 0.019696, Trans_RMSE: 0.140342, Trans_MAE: 0.098947, Trans_R2: 0.762908 +A->B:: Stage: test, Epoch: 1, Loss: 0.172647, Feature_alignment_loss: 0.022388, Cycle_consistency_loss: 0.004171, Scale_consensus_loss: 0.000000, Rot_MSE: 184.323074, Rot_RMSE: 13.576564, Rot_MAE: 8.949026, Rot_R2: -0.091553, Trans_MSE: 0.014960, Trans_RMSE: 0.122311, Trans_MAE: 0.086162, Trans_R2: 0.818860 +A->B:: Stage: best_test, Epoch: 0, Loss: 0.166565, Feature_alignment_loss: 0.025816, Cycle_consistency_loss: 0.003925, Scale_consensus_loss: 0.000000, Rot_MSE: 179.643295, Rot_RMSE: 13.403108, Rot_MAE: 9.078445, Rot_R2: -0.065260, Trans_MSE: 0.015727, Trans_RMSE: 0.125406, Trans_MAE: 0.087686, Trans_R2: 0.809000 +A->B:: Stage: train, Epoch: 2, Loss: 0.129551, Feature_alignment_loss: 0.016067, Cycle_consistency_loss: 0.003961, Scale_consensus_loss: 0.000000, Rot_MSE: 144.258469, Rot_RMSE: 12.010765, Rot_MAE: 7.924441, Rot_R2: 0.143962, Trans_MSE: 0.018057, Trans_RMSE: 0.134376, Trans_MAE: 0.094719, Trans_R2: 0.782601 +A->B:: Stage: test, Epoch: 2, Loss: 0.134246, Feature_alignment_loss: 0.016642, Cycle_consistency_loss: 0.005216, Scale_consensus_loss: 0.000000, Rot_MSE: 121.492859, Rot_RMSE: 11.022380, Rot_MAE: 7.398189, Rot_R2: 0.279314, Trans_MSE: 0.016199, Trans_RMSE: 0.127274, Trans_MAE: 0.087189, Trans_R2: 0.803919 +A->B:: Stage: best_test, Epoch: 2, Loss: 0.134246, Feature_alignment_loss: 0.016642, Cycle_consistency_loss: 0.005216, Scale_consensus_loss: 0.000000, Rot_MSE: 121.492859, Rot_RMSE: 11.022380, Rot_MAE: 7.398189, Rot_R2: 0.279314, Trans_MSE: 0.016199, Trans_RMSE: 0.127274, Trans_MAE: 0.087189, Trans_R2: 0.803919 +A->B:: Stage: train, Epoch: 3, Loss: 0.120033, Feature_alignment_loss: 0.014337, Cycle_consistency_loss: 0.004830, Scale_consensus_loss: 0.000000, Rot_MSE: 136.005890, Rot_RMSE: 11.662156, Rot_MAE: 7.326164, Rot_R2: 0.192546, Trans_MSE: 0.015901, Trans_RMSE: 0.126098, Trans_MAE: 0.089600, Trans_R2: 0.808519 +A->B:: Stage: test, Epoch: 3, Loss: 0.151015, Feature_alignment_loss: 0.017017, Cycle_consistency_loss: 0.005605, Scale_consensus_loss: 0.000000, Rot_MSE: 151.719452, Rot_RMSE: 12.317445, Rot_MAE: 7.628141, Rot_R2: 0.102665, Trans_MSE: 0.015072, Trans_RMSE: 0.122768, Trans_MAE: 0.089921, Trans_R2: 0.816428 +A->B:: Stage: best_test, Epoch: 2, Loss: 0.134246, Feature_alignment_loss: 0.016642, Cycle_consistency_loss: 0.005216, Scale_consensus_loss: 0.000000, Rot_MSE: 121.492859, Rot_RMSE: 11.022380, Rot_MAE: 7.398189, Rot_R2: 0.279314, Trans_MSE: 0.016199, Trans_RMSE: 0.127274, Trans_MAE: 0.087189, Trans_R2: 0.803919 +A->B:: Stage: train, Epoch: 4, Loss: 0.085991, Feature_alignment_loss: 0.011585, Cycle_consistency_loss: 0.004563, Scale_consensus_loss: 0.000000, Rot_MSE: 82.179367, Rot_RMSE: 9.065284, Rot_MAE: 5.812633, Rot_R2: 0.513067, Trans_MSE: 0.011954, Trans_RMSE: 0.109335, Trans_MAE: 0.078599, Trans_R2: 0.856007 +A->B:: Stage: test, Epoch: 4, Loss: 0.092691, Feature_alignment_loss: 0.010410, Cycle_consistency_loss: 0.004407, Scale_consensus_loss: 0.000000, Rot_MSE: 69.282845, Rot_RMSE: 8.323631, Rot_MAE: 5.226562, Rot_R2: 0.588968, Trans_MSE: 0.009185, Trans_RMSE: 0.095839, Trans_MAE: 0.069896, Trans_R2: 0.889199 +A->B:: Stage: best_test, Epoch: 4, Loss: 0.092691, Feature_alignment_loss: 0.010410, Cycle_consistency_loss: 0.004407, Scale_consensus_loss: 0.000000, Rot_MSE: 69.282845, Rot_RMSE: 8.323631, Rot_MAE: 5.226562, Rot_R2: 0.588968, Trans_MSE: 0.009185, Trans_RMSE: 0.095839, Trans_MAE: 0.069896, Trans_R2: 0.889199 +A->B:: Stage: train, Epoch: 5, Loss: 0.070065, Feature_alignment_loss: 0.009725, Cycle_consistency_loss: 0.004429, Scale_consensus_loss: 0.000000, Rot_MSE: 63.459805, Rot_RMSE: 7.966166, Rot_MAE: 5.081885, Rot_R2: 0.624102, Trans_MSE: 0.009228, Trans_RMSE: 0.096062, Trans_MAE: 0.067701, Trans_R2: 0.888879 +A->B:: Stage: test, Epoch: 5, Loss: 0.086186, Feature_alignment_loss: 0.009187, Cycle_consistency_loss: 0.003273, Scale_consensus_loss: 0.000000, Rot_MSE: 70.545052, Rot_RMSE: 8.399110, Rot_MAE: 4.990373, Rot_R2: 0.580991, Trans_MSE: 0.005541, Trans_RMSE: 0.074441, Trans_MAE: 0.051608, Trans_R2: 0.932769 +A->B:: Stage: best_test, Epoch: 5, Loss: 0.086186, Feature_alignment_loss: 0.009187, Cycle_consistency_loss: 0.003273, Scale_consensus_loss: 0.000000, Rot_MSE: 70.545052, Rot_RMSE: 8.399110, Rot_MAE: 4.990373, Rot_R2: 0.580991, Trans_MSE: 0.005541, Trans_RMSE: 0.074441, Trans_MAE: 0.051608, Trans_R2: 0.932769 +A->B:: Stage: train, Epoch: 6, Loss: 0.062978, Feature_alignment_loss: 0.008722, Cycle_consistency_loss: 0.004359, Scale_consensus_loss: 0.000000, Rot_MSE: 57.554562, Rot_RMSE: 7.586473, Rot_MAE: 4.751721, Rot_R2: 0.658759, Trans_MSE: 0.007992, Trans_RMSE: 0.089400, Trans_MAE: 0.061828, Trans_R2: 0.903766 +A->B:: Stage: test, Epoch: 6, Loss: 0.083736, Feature_alignment_loss: 0.009734, Cycle_consistency_loss: 0.003386, Scale_consensus_loss: 0.000000, Rot_MSE: 68.471985, Rot_RMSE: 8.274780, Rot_MAE: 4.816195, Rot_R2: 0.593545, Trans_MSE: 0.005550, Trans_RMSE: 0.074497, Trans_MAE: 0.051284, Trans_R2: 0.932706 +A->B:: Stage: best_test, Epoch: 6, Loss: 0.083736, Feature_alignment_loss: 0.009734, Cycle_consistency_loss: 0.003386, Scale_consensus_loss: 0.000000, Rot_MSE: 68.471985, Rot_RMSE: 8.274780, Rot_MAE: 4.816195, Rot_R2: 0.593545, Trans_MSE: 0.005550, Trans_RMSE: 0.074497, Trans_MAE: 0.051284, Trans_R2: 0.932706 +A->B:: Stage: train, Epoch: 7, Loss: 0.049390, Feature_alignment_loss: 0.007755, Cycle_consistency_loss: 0.004043, Scale_consensus_loss: 0.000000, Rot_MSE: 45.639359, Rot_RMSE: 6.755691, Rot_MAE: 4.167243, Rot_R2: 0.729428, Trans_MSE: 0.005469, Trans_RMSE: 0.073954, Trans_MAE: 0.050834, Trans_R2: 0.934127 +A->B:: Stage: test, Epoch: 7, Loss: 0.075902, Feature_alignment_loss: 0.009225, Cycle_consistency_loss: 0.003184, Scale_consensus_loss: 0.000000, Rot_MSE: 63.912506, Rot_RMSE: 7.994530, Rot_MAE: 4.878172, Rot_R2: 0.620473, Trans_MSE: 0.004415, Trans_RMSE: 0.066447, Trans_MAE: 0.045512, Trans_R2: 0.946193 +A->B:: Stage: best_test, Epoch: 7, Loss: 0.075902, Feature_alignment_loss: 0.009225, Cycle_consistency_loss: 0.003184, Scale_consensus_loss: 0.000000, Rot_MSE: 63.912506, Rot_RMSE: 7.994530, Rot_MAE: 4.878172, Rot_R2: 0.620473, Trans_MSE: 0.004415, Trans_RMSE: 0.066447, Trans_MAE: 0.045512, Trans_R2: 0.946193 +A->B:: Stage: train, Epoch: 8, Loss: 0.049428, Feature_alignment_loss: 0.007355, Cycle_consistency_loss: 0.004126, Scale_consensus_loss: 0.000000, Rot_MSE: 48.444592, Rot_RMSE: 6.960215, Rot_MAE: 4.207371, Rot_R2: 0.712622, Trans_MSE: 0.005142, Trans_RMSE: 0.071705, Trans_MAE: 0.049190, Trans_R2: 0.938075 +A->B:: Stage: test, Epoch: 8, Loss: 0.070283, Feature_alignment_loss: 0.007020, Cycle_consistency_loss: 0.003123, Scale_consensus_loss: 0.000000, Rot_MSE: 70.916550, Rot_RMSE: 8.421196, Rot_MAE: 4.745255, Rot_R2: 0.579021, Trans_MSE: 0.003462, Trans_RMSE: 0.058837, Trans_MAE: 0.038622, Trans_R2: 0.958029 +A->B:: Stage: best_test, Epoch: 8, Loss: 0.070283, Feature_alignment_loss: 0.007020, Cycle_consistency_loss: 0.003123, Scale_consensus_loss: 0.000000, Rot_MSE: 70.916550, Rot_RMSE: 8.421196, Rot_MAE: 4.745255, Rot_R2: 0.579021, Trans_MSE: 0.003462, Trans_RMSE: 0.058837, Trans_MAE: 0.038622, Trans_R2: 0.958029 +A->B:: Stage: train, Epoch: 9, Loss: 0.039070, Feature_alignment_loss: 0.006167, Cycle_consistency_loss: 0.003850, Scale_consensus_loss: 0.000000, Rot_MSE: 36.910526, Rot_RMSE: 6.075403, Rot_MAE: 3.721686, Rot_R2: 0.781391, Trans_MSE: 0.003532, Trans_RMSE: 0.059432, Trans_MAE: 0.040276, Trans_R2: 0.957458 +A->B:: Stage: test, Epoch: 9, Loss: 0.069589, Feature_alignment_loss: 0.006620, Cycle_consistency_loss: 0.002779, Scale_consensus_loss: 0.000000, Rot_MSE: 61.154858, Rot_RMSE: 7.820157, Rot_MAE: 4.560371, Rot_R2: 0.636662, Trans_MSE: 0.002883, Trans_RMSE: 0.053690, Trans_MAE: 0.035114, Trans_R2: 0.964985 +A->B:: Stage: best_test, Epoch: 9, Loss: 0.069589, Feature_alignment_loss: 0.006620, Cycle_consistency_loss: 0.002779, Scale_consensus_loss: 0.000000, Rot_MSE: 61.154858, Rot_RMSE: 7.820157, Rot_MAE: 4.560371, Rot_R2: 0.636662, Trans_MSE: 0.002883, Trans_RMSE: 0.053690, Trans_MAE: 0.035114, Trans_R2: 0.964985 +A->B:: Stage: train, Epoch: 10, Loss: 0.045224, Feature_alignment_loss: 0.006095, Cycle_consistency_loss: 0.004098, Scale_consensus_loss: 0.000000, Rot_MSE: 43.235893, Rot_RMSE: 6.575401, Rot_MAE: 4.092942, Rot_R2: 0.743794, Trans_MSE: 0.004454, Trans_RMSE: 0.066736, Trans_MAE: 0.044690, Trans_R2: 0.946366 +A->B:: Stage: test, Epoch: 10, Loss: 0.059794, Feature_alignment_loss: 0.005343, Cycle_consistency_loss: 0.002739, Scale_consensus_loss: 0.000000, Rot_MSE: 49.200508, Rot_RMSE: 7.014307, Rot_MAE: 4.031521, Rot_R2: 0.707893, Trans_MSE: 0.002570, Trans_RMSE: 0.050696, Trans_MAE: 0.034441, Trans_R2: 0.968767 +A->B:: Stage: best_test, Epoch: 10, Loss: 0.059794, Feature_alignment_loss: 0.005343, Cycle_consistency_loss: 0.002739, Scale_consensus_loss: 0.000000, Rot_MSE: 49.200508, Rot_RMSE: 7.014307, Rot_MAE: 4.031521, Rot_R2: 0.707893, Trans_MSE: 0.002570, Trans_RMSE: 0.050696, Trans_MAE: 0.034441, Trans_R2: 0.968767 +A->B:: Stage: train, Epoch: 11, Loss: 0.036073, Feature_alignment_loss: 0.005117, Cycle_consistency_loss: 0.003826, Scale_consensus_loss: 0.000000, Rot_MSE: 34.115284, Rot_RMSE: 5.840829, Rot_MAE: 3.612137, Rot_R2: 0.798048, Trans_MSE: 0.003273, Trans_RMSE: 0.057212, Trans_MAE: 0.038671, Trans_R2: 0.960575 +A->B:: Stage: test, Epoch: 11, Loss: 0.064986, Feature_alignment_loss: 0.005082, Cycle_consistency_loss: 0.003037, Scale_consensus_loss: 0.000000, Rot_MSE: 61.456974, Rot_RMSE: 7.839450, Rot_MAE: 4.406898, Rot_R2: 0.634837, Trans_MSE: 0.002984, Trans_RMSE: 0.054626, Trans_MAE: 0.036972, Trans_R2: 0.963802 +A->B:: Stage: best_test, Epoch: 10, Loss: 0.059794, Feature_alignment_loss: 0.005343, Cycle_consistency_loss: 0.002739, Scale_consensus_loss: 0.000000, Rot_MSE: 49.200508, Rot_RMSE: 7.014307, Rot_MAE: 4.031521, Rot_R2: 0.707893, Trans_MSE: 0.002570, Trans_RMSE: 0.050696, Trans_MAE: 0.034441, Trans_R2: 0.968767 +A->B:: Stage: train, Epoch: 12, Loss: 0.044833, Feature_alignment_loss: 0.005091, Cycle_consistency_loss: 0.004107, Scale_consensus_loss: 0.000000, Rot_MSE: 44.840679, Rot_RMSE: 6.696318, Rot_MAE: 4.017348, Rot_R2: 0.734303, Trans_MSE: 0.004292, Trans_RMSE: 0.065512, Trans_MAE: 0.043078, Trans_R2: 0.948314 +A->B:: Stage: test, Epoch: 12, Loss: 0.235154, Feature_alignment_loss: 0.007406, Cycle_consistency_loss: 0.012610, Scale_consensus_loss: 0.000000, Rot_MSE: 270.785736, Rot_RMSE: 16.455568, Rot_MAE: 10.716694, Rot_R2: -0.597875, Trans_MSE: 0.034077, Trans_RMSE: 0.184601, Trans_MAE: 0.139569, Trans_R2: 0.584995 +A->B:: Stage: best_test, Epoch: 10, Loss: 0.059794, Feature_alignment_loss: 0.005343, Cycle_consistency_loss: 0.002739, Scale_consensus_loss: 0.000000, Rot_MSE: 49.200508, Rot_RMSE: 7.014307, Rot_MAE: 4.031521, Rot_R2: 0.707893, Trans_MSE: 0.002570, Trans_RMSE: 0.050696, Trans_MAE: 0.034441, Trans_R2: 0.968767 +A->B:: Stage: train, Epoch: 13, Loss: 0.062737, Feature_alignment_loss: 0.005718, Cycle_consistency_loss: 0.004702, Scale_consensus_loss: 0.000000, Rot_MSE: 64.392570, Rot_RMSE: 8.024498, Rot_MAE: 5.068717, Rot_R2: 0.617904, Trans_MSE: 0.006965, Trans_RMSE: 0.083459, Trans_MAE: 0.058287, Trans_R2: 0.916130 +A->B:: Stage: test, Epoch: 13, Loss: 0.057452, Feature_alignment_loss: 0.005012, Cycle_consistency_loss: 0.003100, Scale_consensus_loss: 0.000000, Rot_MSE: 51.388565, Rot_RMSE: 7.168582, Rot_MAE: 4.253785, Rot_R2: 0.694849, Trans_MSE: 0.003379, Trans_RMSE: 0.058128, Trans_MAE: 0.040444, Trans_R2: 0.959025 +A->B:: Stage: best_test, Epoch: 13, Loss: 0.057452, Feature_alignment_loss: 0.005012, Cycle_consistency_loss: 0.003100, Scale_consensus_loss: 0.000000, Rot_MSE: 51.388565, Rot_RMSE: 7.168582, Rot_MAE: 4.253785, Rot_R2: 0.694849, Trans_MSE: 0.003379, Trans_RMSE: 0.058128, Trans_MAE: 0.040444, Trans_R2: 0.959025 +A->B:: Stage: train, Epoch: 14, Loss: 0.037415, Feature_alignment_loss: 0.004757, Cycle_consistency_loss: 0.003797, Scale_consensus_loss: 0.000000, Rot_MSE: 38.449165, Rot_RMSE: 6.200739, Rot_MAE: 3.871141, Rot_R2: 0.772291, Trans_MSE: 0.003185, Trans_RMSE: 0.056438, Trans_MAE: 0.038968, Trans_R2: 0.961639 +A->B:: Stage: test, Epoch: 14, Loss: 0.054532, Feature_alignment_loss: 0.004705, Cycle_consistency_loss: 0.002754, Scale_consensus_loss: 0.000000, Rot_MSE: 47.255913, Rot_RMSE: 6.874294, Rot_MAE: 4.015331, Rot_R2: 0.719199, Trans_MSE: 0.002510, Trans_RMSE: 0.050105, Trans_MAE: 0.033999, Trans_R2: 0.969543 +A->B:: Stage: best_test, Epoch: 14, Loss: 0.054532, Feature_alignment_loss: 0.004705, Cycle_consistency_loss: 0.002754, Scale_consensus_loss: 0.000000, Rot_MSE: 47.255913, Rot_RMSE: 6.874294, Rot_MAE: 4.015331, Rot_R2: 0.719199, Trans_MSE: 0.002510, Trans_RMSE: 0.050105, Trans_MAE: 0.033999, Trans_R2: 0.969543 +A->B:: Stage: train, Epoch: 15, Loss: 0.032436, Feature_alignment_loss: 0.004239, Cycle_consistency_loss: 0.003702, Scale_consensus_loss: 0.000000, Rot_MSE: 32.009171, Rot_RMSE: 5.657665, Rot_MAE: 3.532316, Rot_R2: 0.810613, Trans_MSE: 0.002639, Trans_RMSE: 0.051370, Trans_MAE: 0.034846, Trans_R2: 0.968223 +A->B:: Stage: test, Epoch: 15, Loss: 0.055424, Feature_alignment_loss: 0.004138, Cycle_consistency_loss: 0.002753, Scale_consensus_loss: 0.000000, Rot_MSE: 54.363628, Rot_RMSE: 7.373169, Rot_MAE: 4.164560, Rot_R2: 0.676673, Trans_MSE: 0.002424, Trans_RMSE: 0.049235, Trans_MAE: 0.033455, Trans_R2: 0.970545 +A->B:: Stage: best_test, Epoch: 14, Loss: 0.054532, Feature_alignment_loss: 0.004705, Cycle_consistency_loss: 0.002754, Scale_consensus_loss: 0.000000, Rot_MSE: 47.255913, Rot_RMSE: 6.874294, Rot_MAE: 4.015331, Rot_R2: 0.719199, Trans_MSE: 0.002510, Trans_RMSE: 0.050105, Trans_MAE: 0.033999, Trans_R2: 0.969543 +A->B:: Stage: train, Epoch: 16, Loss: 0.031591, Feature_alignment_loss: 0.004145, Cycle_consistency_loss: 0.003649, Scale_consensus_loss: 0.000000, Rot_MSE: 31.410295, Rot_RMSE: 5.604489, Rot_MAE: 3.463261, Rot_R2: 0.814057, Trans_MSE: 0.002653, Trans_RMSE: 0.051508, Trans_MAE: 0.035220, Trans_R2: 0.968048 +A->B:: Stage: test, Epoch: 16, Loss: 0.050014, Feature_alignment_loss: 0.004131, Cycle_consistency_loss: 0.002831, Scale_consensus_loss: 0.000000, Rot_MSE: 48.187843, Rot_RMSE: 6.941746, Rot_MAE: 3.930924, Rot_R2: 0.713701, Trans_MSE: 0.002201, Trans_RMSE: 0.046913, Trans_MAE: 0.031652, Trans_R2: 0.973290 +A->B:: Stage: best_test, Epoch: 16, Loss: 0.050014, Feature_alignment_loss: 0.004131, Cycle_consistency_loss: 0.002831, Scale_consensus_loss: 0.000000, Rot_MSE: 48.187843, Rot_RMSE: 6.941746, Rot_MAE: 3.930924, Rot_R2: 0.713701, Trans_MSE: 0.002201, Trans_RMSE: 0.046913, Trans_MAE: 0.031652, Trans_R2: 0.973290 +A->B:: Stage: train, Epoch: 17, Loss: 0.029406, Feature_alignment_loss: 0.003979, Cycle_consistency_loss: 0.003633, Scale_consensus_loss: 0.000000, Rot_MSE: 29.694199, Rot_RMSE: 5.449238, Rot_MAE: 3.353605, Rot_R2: 0.824150, Trans_MSE: 0.002447, Trans_RMSE: 0.049469, Trans_MAE: 0.033537, Trans_R2: 0.970532 +A->B:: Stage: test, Epoch: 17, Loss: 0.059463, Feature_alignment_loss: 0.004524, Cycle_consistency_loss: 0.002868, Scale_consensus_loss: 0.000000, Rot_MSE: 47.336246, Rot_RMSE: 6.880134, Rot_MAE: 4.052771, Rot_R2: 0.718835, Trans_MSE: 0.003105, Trans_RMSE: 0.055722, Trans_MAE: 0.036837, Trans_R2: 0.962189 +A->B:: Stage: best_test, Epoch: 16, Loss: 0.050014, Feature_alignment_loss: 0.004131, Cycle_consistency_loss: 0.002831, Scale_consensus_loss: 0.000000, Rot_MSE: 48.187843, Rot_RMSE: 6.941746, Rot_MAE: 3.930924, Rot_R2: 0.713701, Trans_MSE: 0.002201, Trans_RMSE: 0.046913, Trans_MAE: 0.031652, Trans_R2: 0.973290 +A->B:: Stage: train, Epoch: 18, Loss: 0.032352, Feature_alignment_loss: 0.004082, Cycle_consistency_loss: 0.003700, Scale_consensus_loss: 0.000000, Rot_MSE: 32.746540, Rot_RMSE: 5.722459, Rot_MAE: 3.537093, Rot_R2: 0.806167, Trans_MSE: 0.002807, Trans_RMSE: 0.052985, Trans_MAE: 0.036076, Trans_R2: 0.966191 +A->B:: Stage: test, Epoch: 18, Loss: 0.059860, Feature_alignment_loss: 0.004179, Cycle_consistency_loss: 0.003228, Scale_consensus_loss: 0.000000, Rot_MSE: 49.906021, Rot_RMSE: 7.064419, Rot_MAE: 4.096198, Rot_R2: 0.703658, Trans_MSE: 0.003706, Trans_RMSE: 0.060876, Trans_MAE: 0.041514, Trans_R2: 0.954858 +A->B:: Stage: best_test, Epoch: 16, Loss: 0.050014, Feature_alignment_loss: 0.004131, Cycle_consistency_loss: 0.002831, Scale_consensus_loss: 0.000000, Rot_MSE: 48.187843, Rot_RMSE: 6.941746, Rot_MAE: 3.930924, Rot_R2: 0.713701, Trans_MSE: 0.002201, Trans_RMSE: 0.046913, Trans_MAE: 0.031652, Trans_R2: 0.973290 +A->B:: Stage: train, Epoch: 19, Loss: 0.028910, Feature_alignment_loss: 0.003832, Cycle_consistency_loss: 0.003534, Scale_consensus_loss: 0.000000, Rot_MSE: 29.114525, Rot_RMSE: 5.395788, Rot_MAE: 3.312443, Rot_R2: 0.827674, Trans_MSE: 0.002424, Trans_RMSE: 0.049236, Trans_MAE: 0.033337, Trans_R2: 0.970805 +A->B:: Stage: test, Epoch: 19, Loss: 0.054074, Feature_alignment_loss: 0.004237, Cycle_consistency_loss: 0.002537, Scale_consensus_loss: 0.000000, Rot_MSE: 44.319218, Rot_RMSE: 6.657268, Rot_MAE: 3.751289, Rot_R2: 0.736420, Trans_MSE: 0.002202, Trans_RMSE: 0.046924, Trans_MAE: 0.031546, Trans_R2: 0.973247 +A->B:: Stage: best_test, Epoch: 16, Loss: 0.050014, Feature_alignment_loss: 0.004131, Cycle_consistency_loss: 0.002831, Scale_consensus_loss: 0.000000, Rot_MSE: 48.187843, Rot_RMSE: 6.941746, Rot_MAE: 3.930924, Rot_R2: 0.713701, Trans_MSE: 0.002201, Trans_RMSE: 0.046913, Trans_MAE: 0.031652, Trans_R2: 0.973290 +A->B:: Stage: train, Epoch: 20, Loss: 0.028183, Feature_alignment_loss: 0.003772, Cycle_consistency_loss: 0.003563, Scale_consensus_loss: 0.000000, Rot_MSE: 28.165556, Rot_RMSE: 5.307123, Rot_MAE: 3.257360, Rot_R2: 0.833277, Trans_MSE: 0.002330, Trans_RMSE: 0.048272, Trans_MAE: 0.032763, Trans_R2: 0.971937 +A->B:: Stage: test, Epoch: 20, Loss: 0.050203, Feature_alignment_loss: 0.003329, Cycle_consistency_loss: 0.002560, Scale_consensus_loss: 0.000000, Rot_MSE: 41.345360, Rot_RMSE: 6.430036, Rot_MAE: 3.733119, Rot_R2: 0.754328, Trans_MSE: 0.002718, Trans_RMSE: 0.052134, Trans_MAE: 0.035574, Trans_R2: 0.966939 +A->B:: Stage: best_test, Epoch: 16, Loss: 0.050014, Feature_alignment_loss: 0.004131, Cycle_consistency_loss: 0.002831, Scale_consensus_loss: 0.000000, Rot_MSE: 48.187843, Rot_RMSE: 6.941746, Rot_MAE: 3.930924, Rot_R2: 0.713701, Trans_MSE: 0.002201, Trans_RMSE: 0.046913, Trans_MAE: 0.031652, Trans_R2: 0.973290 +A->B:: Stage: train, Epoch: 21, Loss: 0.027494, Feature_alignment_loss: 0.003593, Cycle_consistency_loss: 0.003498, Scale_consensus_loss: 0.000000, Rot_MSE: 27.785835, Rot_RMSE: 5.271227, Rot_MAE: 3.214384, Rot_R2: 0.835561, Trans_MSE: 0.002228, Trans_RMSE: 0.047201, Trans_MAE: 0.031892, Trans_R2: 0.973169 +A->B:: Stage: test, Epoch: 21, Loss: 0.048111, Feature_alignment_loss: 0.003851, Cycle_consistency_loss: 0.002709, Scale_consensus_loss: 0.000000, Rot_MSE: 40.541458, Rot_RMSE: 6.367218, Rot_MAE: 3.597621, Rot_R2: 0.759186, Trans_MSE: 0.001991, Trans_RMSE: 0.044618, Trans_MAE: 0.029571, Trans_R2: 0.975797 +A->B:: Stage: best_test, Epoch: 21, Loss: 0.048111, Feature_alignment_loss: 0.003851, Cycle_consistency_loss: 0.002709, Scale_consensus_loss: 0.000000, Rot_MSE: 40.541458, Rot_RMSE: 6.367218, Rot_MAE: 3.597621, Rot_R2: 0.759186, Trans_MSE: 0.001991, Trans_RMSE: 0.044618, Trans_MAE: 0.029571, Trans_R2: 0.975797 +A->B:: Stage: train, Epoch: 22, Loss: 0.027268, Feature_alignment_loss: 0.003552, Cycle_consistency_loss: 0.003507, Scale_consensus_loss: 0.000000, Rot_MSE: 27.648565, Rot_RMSE: 5.258190, Rot_MAE: 3.203187, Rot_R2: 0.836252, Trans_MSE: 0.002294, Trans_RMSE: 0.047894, Trans_MAE: 0.032406, Trans_R2: 0.972377 +A->B:: Stage: test, Epoch: 22, Loss: 0.043596, Feature_alignment_loss: 0.003570, Cycle_consistency_loss: 0.002706, Scale_consensus_loss: 0.000000, Rot_MSE: 39.522041, Rot_RMSE: 6.286656, Rot_MAE: 3.573402, Rot_R2: 0.765149, Trans_MSE: 0.002065, Trans_RMSE: 0.045437, Trans_MAE: 0.030017, Trans_R2: 0.974917 +A->B:: Stage: best_test, Epoch: 22, Loss: 0.043596, Feature_alignment_loss: 0.003570, Cycle_consistency_loss: 0.002706, Scale_consensus_loss: 0.000000, Rot_MSE: 39.522041, Rot_RMSE: 6.286656, Rot_MAE: 3.573402, Rot_R2: 0.765149, Trans_MSE: 0.002065, Trans_RMSE: 0.045437, Trans_MAE: 0.030017, Trans_R2: 0.974917 +A->B:: Stage: train, Epoch: 23, Loss: 0.026779, Feature_alignment_loss: 0.003489, Cycle_consistency_loss: 0.003537, Scale_consensus_loss: 0.000000, Rot_MSE: 26.411772, Rot_RMSE: 5.139238, Rot_MAE: 3.134944, Rot_R2: 0.843601, Trans_MSE: 0.002205, Trans_RMSE: 0.046955, Trans_MAE: 0.031792, Trans_R2: 0.973447 +A->B:: Stage: test, Epoch: 23, Loss: 0.043824, Feature_alignment_loss: 0.003819, Cycle_consistency_loss: 0.002515, Scale_consensus_loss: 0.000000, Rot_MSE: 37.388084, Rot_RMSE: 6.114580, Rot_MAE: 3.485321, Rot_R2: 0.777873, Trans_MSE: 0.001981, Trans_RMSE: 0.044509, Trans_MAE: 0.029463, Trans_R2: 0.975847 +A->B:: Stage: best_test, Epoch: 22, Loss: 0.043596, Feature_alignment_loss: 0.003570, Cycle_consistency_loss: 0.002706, Scale_consensus_loss: 0.000000, Rot_MSE: 39.522041, Rot_RMSE: 6.286656, Rot_MAE: 3.573402, Rot_R2: 0.765149, Trans_MSE: 0.002065, Trans_RMSE: 0.045437, Trans_MAE: 0.030017, Trans_R2: 0.974917 +A->B:: Stage: train, Epoch: 24, Loss: 0.026298, Feature_alignment_loss: 0.003443, Cycle_consistency_loss: 0.003516, Scale_consensus_loss: 0.000000, Rot_MSE: 26.017794, Rot_RMSE: 5.100764, Rot_MAE: 3.139046, Rot_R2: 0.846018, Trans_MSE: 0.002089, Trans_RMSE: 0.045702, Trans_MAE: 0.030946, Trans_R2: 0.974846 +A->B:: Stage: test, Epoch: 24, Loss: 0.044895, Feature_alignment_loss: 0.003408, Cycle_consistency_loss: 0.002547, Scale_consensus_loss: 0.000000, Rot_MSE: 35.710129, Rot_RMSE: 5.975795, Rot_MAE: 3.402477, Rot_R2: 0.787879, Trans_MSE: 0.002483, Trans_RMSE: 0.049826, Trans_MAE: 0.033079, Trans_R2: 0.969817 +A->B:: Stage: best_test, Epoch: 22, Loss: 0.043596, Feature_alignment_loss: 0.003570, Cycle_consistency_loss: 0.002706, Scale_consensus_loss: 0.000000, Rot_MSE: 39.522041, Rot_RMSE: 6.286656, Rot_MAE: 3.573402, Rot_R2: 0.765149, Trans_MSE: 0.002065, Trans_RMSE: 0.045437, Trans_MAE: 0.030017, Trans_R2: 0.974917 +A->B:: Stage: train, Epoch: 25, Loss: 0.030488, Feature_alignment_loss: 0.003524, Cycle_consistency_loss: 0.003721, Scale_consensus_loss: 0.000000, Rot_MSE: 30.809853, Rot_RMSE: 5.550663, Rot_MAE: 3.412954, Rot_R2: 0.817605, Trans_MSE: 0.002728, Trans_RMSE: 0.052231, Trans_MAE: 0.035168, Trans_R2: 0.967147 +A->B:: Stage: test, Epoch: 25, Loss: 0.047397, Feature_alignment_loss: 0.003195, Cycle_consistency_loss: 0.002387, Scale_consensus_loss: 0.000000, Rot_MSE: 39.722912, Rot_RMSE: 6.302611, Rot_MAE: 3.504935, Rot_R2: 0.763900, Trans_MSE: 0.002375, Trans_RMSE: 0.048738, Trans_MAE: 0.032606, Trans_R2: 0.971174 +A->B:: Stage: best_test, Epoch: 22, Loss: 0.043596, Feature_alignment_loss: 0.003570, Cycle_consistency_loss: 0.002706, Scale_consensus_loss: 0.000000, Rot_MSE: 39.522041, Rot_RMSE: 6.286656, Rot_MAE: 3.573402, Rot_R2: 0.765149, Trans_MSE: 0.002065, Trans_RMSE: 0.045437, Trans_MAE: 0.030017, Trans_R2: 0.974917 +A->B:: Stage: train, Epoch: 26, Loss: 0.026535, Feature_alignment_loss: 0.003351, Cycle_consistency_loss: 0.003461, Scale_consensus_loss: 0.000000, Rot_MSE: 27.868643, Rot_RMSE: 5.279076, Rot_MAE: 3.127464, Rot_R2: 0.834913, Trans_MSE: 0.002114, Trans_RMSE: 0.045982, Trans_MAE: 0.030872, Trans_R2: 0.974535 +A->B:: Stage: test, Epoch: 26, Loss: 0.045873, Feature_alignment_loss: 0.003396, Cycle_consistency_loss: 0.002399, Scale_consensus_loss: 0.000000, Rot_MSE: 35.921680, Rot_RMSE: 5.993470, Rot_MAE: 3.367178, Rot_R2: 0.786353, Trans_MSE: 0.001899, Trans_RMSE: 0.043580, Trans_MAE: 0.028870, Trans_R2: 0.976925 +A->B:: Stage: best_test, Epoch: 22, Loss: 0.043596, Feature_alignment_loss: 0.003570, Cycle_consistency_loss: 0.002706, Scale_consensus_loss: 0.000000, Rot_MSE: 39.522041, Rot_RMSE: 6.286656, Rot_MAE: 3.573402, Rot_R2: 0.765149, Trans_MSE: 0.002065, Trans_RMSE: 0.045437, Trans_MAE: 0.030017, Trans_R2: 0.974917 +A->B:: Stage: train, Epoch: 27, Loss: 0.028456, Feature_alignment_loss: 0.003445, Cycle_consistency_loss: 0.003436, Scale_consensus_loss: 0.000000, Rot_MSE: 28.013346, Rot_RMSE: 5.292764, Rot_MAE: 3.220472, Rot_R2: 0.834243, Trans_MSE: 0.002419, Trans_RMSE: 0.049179, Trans_MAE: 0.032967, Trans_R2: 0.970877 +A->B:: Stage: test, Epoch: 27, Loss: 0.045758, Feature_alignment_loss: 0.003345, Cycle_consistency_loss: 0.002396, Scale_consensus_loss: 0.000000, Rot_MSE: 39.486084, Rot_RMSE: 6.283795, Rot_MAE: 3.494423, Rot_R2: 0.765145, Trans_MSE: 0.001828, Trans_RMSE: 0.042756, Trans_MAE: 0.028580, Trans_R2: 0.977731 +A->B:: Stage: best_test, Epoch: 22, Loss: 0.043596, Feature_alignment_loss: 0.003570, Cycle_consistency_loss: 0.002706, Scale_consensus_loss: 0.000000, Rot_MSE: 39.522041, Rot_RMSE: 6.286656, Rot_MAE: 3.573402, Rot_R2: 0.765149, Trans_MSE: 0.002065, Trans_RMSE: 0.045437, Trans_MAE: 0.030017, Trans_R2: 0.974917 +A->B:: Stage: train, Epoch: 28, Loss: 0.024091, Feature_alignment_loss: 0.003168, Cycle_consistency_loss: 0.003299, Scale_consensus_loss: 0.000000, Rot_MSE: 24.170063, Rot_RMSE: 4.916306, Rot_MAE: 2.967035, Rot_R2: 0.856998, Trans_MSE: 0.001853, Trans_RMSE: 0.043044, Trans_MAE: 0.029035, Trans_R2: 0.977685 +A->B:: Stage: test, Epoch: 28, Loss: 0.041392, Feature_alignment_loss: 0.002991, Cycle_consistency_loss: 0.002421, Scale_consensus_loss: 0.000000, Rot_MSE: 34.584663, Rot_RMSE: 5.880873, Rot_MAE: 3.289519, Rot_R2: 0.794373, Trans_MSE: 0.001753, Trans_RMSE: 0.041871, Trans_MAE: 0.027789, Trans_R2: 0.978691 +A->B:: Stage: best_test, Epoch: 28, Loss: 0.041392, Feature_alignment_loss: 0.002991, Cycle_consistency_loss: 0.002421, Scale_consensus_loss: 0.000000, Rot_MSE: 34.584663, Rot_RMSE: 5.880873, Rot_MAE: 3.289519, Rot_R2: 0.794373, Trans_MSE: 0.001753, Trans_RMSE: 0.041871, Trans_MAE: 0.027789, Trans_R2: 0.978691 +A->B:: Stage: train, Epoch: 29, Loss: 0.023653, Feature_alignment_loss: 0.003213, Cycle_consistency_loss: 0.003283, Scale_consensus_loss: 0.000000, Rot_MSE: 22.936661, Rot_RMSE: 4.789223, Rot_MAE: 2.896895, Rot_R2: 0.864394, Trans_MSE: 0.001799, Trans_RMSE: 0.042417, Trans_MAE: 0.028610, Trans_R2: 0.978331 +A->B:: Stage: test, Epoch: 29, Loss: 0.045168, Feature_alignment_loss: 0.003461, Cycle_consistency_loss: 0.002460, Scale_consensus_loss: 0.000000, Rot_MSE: 36.109211, Rot_RMSE: 6.009094, Rot_MAE: 3.268096, Rot_R2: 0.785185, Trans_MSE: 0.001873, Trans_RMSE: 0.043277, Trans_MAE: 0.028947, Trans_R2: 0.977244 +A->B:: Stage: best_test, Epoch: 28, Loss: 0.041392, Feature_alignment_loss: 0.002991, Cycle_consistency_loss: 0.002421, Scale_consensus_loss: 0.000000, Rot_MSE: 34.584663, Rot_RMSE: 5.880873, Rot_MAE: 3.289519, Rot_R2: 0.794373, Trans_MSE: 0.001753, Trans_RMSE: 0.041871, Trans_MAE: 0.027789, Trans_R2: 0.978691 +A->B:: Stage: train, Epoch: 30, Loss: 0.020142, Feature_alignment_loss: 0.002861, Cycle_consistency_loss: 0.003176, Scale_consensus_loss: 0.000000, Rot_MSE: 20.287727, Rot_RMSE: 4.504190, Rot_MAE: 2.701549, Rot_R2: 0.879994, Trans_MSE: 0.001462, Trans_RMSE: 0.038233, Trans_MAE: 0.025298, Trans_R2: 0.982390 +A->B:: Stage: test, Epoch: 30, Loss: 0.037956, Feature_alignment_loss: 0.002734, Cycle_consistency_loss: 0.002205, Scale_consensus_loss: 0.000000, Rot_MSE: 30.981306, Rot_RMSE: 5.566085, Rot_MAE: 3.019931, Rot_R2: 0.815582, Trans_MSE: 0.001459, Trans_RMSE: 0.038197, Trans_MAE: 0.025204, Trans_R2: 0.982289 +A->B:: Stage: best_test, Epoch: 30, Loss: 0.037956, Feature_alignment_loss: 0.002734, Cycle_consistency_loss: 0.002205, Scale_consensus_loss: 0.000000, Rot_MSE: 30.981306, Rot_RMSE: 5.566085, Rot_MAE: 3.019931, Rot_R2: 0.815582, Trans_MSE: 0.001459, Trans_RMSE: 0.038197, Trans_MAE: 0.025204, Trans_R2: 0.982289 +A->B:: Stage: train, Epoch: 31, Loss: 0.019295, Feature_alignment_loss: 0.002813, Cycle_consistency_loss: 0.003112, Scale_consensus_loss: 0.000000, Rot_MSE: 18.555485, Rot_RMSE: 4.307608, Rot_MAE: 2.584682, Rot_R2: 0.890302, Trans_MSE: 0.001392, Trans_RMSE: 0.037305, Trans_MAE: 0.024686, Trans_R2: 0.983238 +A->B:: Stage: test, Epoch: 31, Loss: 0.038748, Feature_alignment_loss: 0.002824, Cycle_consistency_loss: 0.002243, Scale_consensus_loss: 0.000000, Rot_MSE: 31.797871, Rot_RMSE: 5.638960, Rot_MAE: 3.061452, Rot_R2: 0.810758, Trans_MSE: 0.001380, Trans_RMSE: 0.037153, Trans_MAE: 0.024655, Trans_R2: 0.983229 +A->B:: Stage: best_test, Epoch: 30, Loss: 0.037956, Feature_alignment_loss: 0.002734, Cycle_consistency_loss: 0.002205, Scale_consensus_loss: 0.000000, Rot_MSE: 30.981306, Rot_RMSE: 5.566085, Rot_MAE: 3.019931, Rot_R2: 0.815582, Trans_MSE: 0.001459, Trans_RMSE: 0.038197, Trans_MAE: 0.025204, Trans_R2: 0.982289 +A->B:: Stage: train, Epoch: 32, Loss: 0.018993, Feature_alignment_loss: 0.002794, Cycle_consistency_loss: 0.003041, Scale_consensus_loss: 0.000000, Rot_MSE: 17.767298, Rot_RMSE: 4.215127, Rot_MAE: 2.576230, Rot_R2: 0.894967, Trans_MSE: 0.001400, Trans_RMSE: 0.037413, Trans_MAE: 0.024870, Trans_R2: 0.983142 +A->B:: Stage: test, Epoch: 32, Loss: 0.038926, Feature_alignment_loss: 0.002700, Cycle_consistency_loss: 0.002175, Scale_consensus_loss: 0.000000, Rot_MSE: 31.786037, Rot_RMSE: 5.637911, Rot_MAE: 3.004672, Rot_R2: 0.810832, Trans_MSE: 0.001585, Trans_RMSE: 0.039813, Trans_MAE: 0.026374, Trans_R2: 0.980748 +A->B:: Stage: best_test, Epoch: 30, Loss: 0.037956, Feature_alignment_loss: 0.002734, Cycle_consistency_loss: 0.002205, Scale_consensus_loss: 0.000000, Rot_MSE: 30.981306, Rot_RMSE: 5.566085, Rot_MAE: 3.019931, Rot_R2: 0.815582, Trans_MSE: 0.001459, Trans_RMSE: 0.038197, Trans_MAE: 0.025204, Trans_R2: 0.982289 +A->B:: Stage: train, Epoch: 33, Loss: 0.019231, Feature_alignment_loss: 0.002790, Cycle_consistency_loss: 0.003064, Scale_consensus_loss: 0.000000, Rot_MSE: 19.373535, Rot_RMSE: 4.401538, Rot_MAE: 2.603633, Rot_R2: 0.885453, Trans_MSE: 0.001369, Trans_RMSE: 0.036995, Trans_MAE: 0.024583, Trans_R2: 0.983516 +A->B:: Stage: test, Epoch: 33, Loss: 0.038055, Feature_alignment_loss: 0.002723, Cycle_consistency_loss: 0.002220, Scale_consensus_loss: 0.000000, Rot_MSE: 32.326534, Rot_RMSE: 5.685643, Rot_MAE: 3.032421, Rot_R2: 0.807623, Trans_MSE: 0.001479, Trans_RMSE: 0.038463, Trans_MAE: 0.025857, Trans_R2: 0.982032 +A->B:: Stage: best_test, Epoch: 30, Loss: 0.037956, Feature_alignment_loss: 0.002734, Cycle_consistency_loss: 0.002205, Scale_consensus_loss: 0.000000, Rot_MSE: 30.981306, Rot_RMSE: 5.566085, Rot_MAE: 3.019931, Rot_R2: 0.815582, Trans_MSE: 0.001459, Trans_RMSE: 0.038197, Trans_MAE: 0.025204, Trans_R2: 0.982289 +A->B:: Stage: train, Epoch: 34, Loss: 0.019372, Feature_alignment_loss: 0.002796, Cycle_consistency_loss: 0.003027, Scale_consensus_loss: 0.000000, Rot_MSE: 19.666075, Rot_RMSE: 4.434645, Rot_MAE: 2.627086, Rot_R2: 0.883719, Trans_MSE: 0.001359, Trans_RMSE: 0.036869, Trans_MAE: 0.024404, Trans_R2: 0.983627 +A->B:: Stage: test, Epoch: 34, Loss: 0.037508, Feature_alignment_loss: 0.002671, Cycle_consistency_loss: 0.002197, Scale_consensus_loss: 0.000000, Rot_MSE: 30.782272, Rot_RMSE: 5.548177, Rot_MAE: 2.992996, Rot_R2: 0.816824, Trans_MSE: 0.001474, Trans_RMSE: 0.038392, Trans_MAE: 0.025606, Trans_R2: 0.982094 +A->B:: Stage: best_test, Epoch: 34, Loss: 0.037508, Feature_alignment_loss: 0.002671, Cycle_consistency_loss: 0.002197, Scale_consensus_loss: 0.000000, Rot_MSE: 30.782272, Rot_RMSE: 5.548177, Rot_MAE: 2.992996, Rot_R2: 0.816824, Trans_MSE: 0.001474, Trans_RMSE: 0.038392, Trans_MAE: 0.025606, Trans_R2: 0.982094 +A->B:: Stage: train, Epoch: 35, Loss: 0.019020, Feature_alignment_loss: 0.002748, Cycle_consistency_loss: 0.003012, Scale_consensus_loss: 0.000000, Rot_MSE: 18.900715, Rot_RMSE: 4.347495, Rot_MAE: 2.585946, Rot_R2: 0.888344, Trans_MSE: 0.001314, Trans_RMSE: 0.036248, Trans_MAE: 0.024115, Trans_R2: 0.984172 +A->B:: Stage: test, Epoch: 35, Loss: 0.036623, Feature_alignment_loss: 0.002634, Cycle_consistency_loss: 0.002194, Scale_consensus_loss: 0.000000, Rot_MSE: 30.459099, Rot_RMSE: 5.518976, Rot_MAE: 2.946124, Rot_R2: 0.818706, Trans_MSE: 0.001411, Trans_RMSE: 0.037566, Trans_MAE: 0.025030, Trans_R2: 0.982871 +A->B:: Stage: best_test, Epoch: 35, Loss: 0.036623, Feature_alignment_loss: 0.002634, Cycle_consistency_loss: 0.002194, Scale_consensus_loss: 0.000000, Rot_MSE: 30.459099, Rot_RMSE: 5.518976, Rot_MAE: 2.946124, Rot_R2: 0.818706, Trans_MSE: 0.001411, Trans_RMSE: 0.037566, Trans_MAE: 0.025030, Trans_R2: 0.982871 +A->B:: Stage: train, Epoch: 36, Loss: 0.018533, Feature_alignment_loss: 0.002738, Cycle_consistency_loss: 0.002992, Scale_consensus_loss: 0.000000, Rot_MSE: 17.782309, Rot_RMSE: 4.216908, Rot_MAE: 2.551381, Rot_R2: 0.894947, Trans_MSE: 0.001327, Trans_RMSE: 0.036425, Trans_MAE: 0.024304, Trans_R2: 0.984017 +A->B:: Stage: test, Epoch: 36, Loss: 0.036696, Feature_alignment_loss: 0.002764, Cycle_consistency_loss: 0.002182, Scale_consensus_loss: 0.000000, Rot_MSE: 30.672606, Rot_RMSE: 5.538285, Rot_MAE: 2.931363, Rot_R2: 0.817483, Trans_MSE: 0.001340, Trans_RMSE: 0.036610, Trans_MAE: 0.024157, Trans_R2: 0.983715 +A->B:: Stage: best_test, Epoch: 35, Loss: 0.036623, Feature_alignment_loss: 0.002634, Cycle_consistency_loss: 0.002194, Scale_consensus_loss: 0.000000, Rot_MSE: 30.459099, Rot_RMSE: 5.518976, Rot_MAE: 2.946124, Rot_R2: 0.818706, Trans_MSE: 0.001411, Trans_RMSE: 0.037566, Trans_MAE: 0.025030, Trans_R2: 0.982871 +A->B:: Stage: train, Epoch: 37, Loss: 0.018234, Feature_alignment_loss: 0.002724, Cycle_consistency_loss: 0.002963, Scale_consensus_loss: 0.000000, Rot_MSE: 17.001221, Rot_RMSE: 4.123254, Rot_MAE: 2.507087, Rot_R2: 0.899459, Trans_MSE: 0.001350, Trans_RMSE: 0.036741, Trans_MAE: 0.024064, Trans_R2: 0.983738 +A->B:: Stage: test, Epoch: 37, Loss: 0.037324, Feature_alignment_loss: 0.002564, Cycle_consistency_loss: 0.002108, Scale_consensus_loss: 0.000000, Rot_MSE: 30.486889, Rot_RMSE: 5.521493, Rot_MAE: 2.980582, Rot_R2: 0.818592, Trans_MSE: 0.001440, Trans_RMSE: 0.037952, Trans_MAE: 0.025541, Trans_R2: 0.982501 +A->B:: Stage: best_test, Epoch: 35, Loss: 0.036623, Feature_alignment_loss: 0.002634, Cycle_consistency_loss: 0.002194, Scale_consensus_loss: 0.000000, Rot_MSE: 30.459099, Rot_RMSE: 5.518976, Rot_MAE: 2.946124, Rot_R2: 0.818706, Trans_MSE: 0.001411, Trans_RMSE: 0.037566, Trans_MAE: 0.025030, Trans_R2: 0.982871 +A->B:: Stage: train, Epoch: 38, Loss: 0.018609, Feature_alignment_loss: 0.002751, Cycle_consistency_loss: 0.002981, Scale_consensus_loss: 0.000000, Rot_MSE: 17.892448, Rot_RMSE: 4.229947, Rot_MAE: 2.549684, Rot_R2: 0.894280, Trans_MSE: 0.001302, Trans_RMSE: 0.036086, Trans_MAE: 0.023809, Trans_R2: 0.984313 +A->B:: Stage: test, Epoch: 38, Loss: 0.036186, Feature_alignment_loss: 0.002648, Cycle_consistency_loss: 0.002137, Scale_consensus_loss: 0.000000, Rot_MSE: 29.812449, Rot_RMSE: 5.460078, Rot_MAE: 2.936675, Rot_R2: 0.822578, Trans_MSE: 0.001279, Trans_RMSE: 0.035757, Trans_MAE: 0.023811, Trans_R2: 0.984467 +A->B:: Stage: best_test, Epoch: 38, Loss: 0.036186, Feature_alignment_loss: 0.002648, Cycle_consistency_loss: 0.002137, Scale_consensus_loss: 0.000000, Rot_MSE: 29.812449, Rot_RMSE: 5.460078, Rot_MAE: 2.936675, Rot_R2: 0.822578, Trans_MSE: 0.001279, Trans_RMSE: 0.035757, Trans_MAE: 0.023811, Trans_R2: 0.984467 +A->B:: Stage: train, Epoch: 39, Loss: 0.018696, Feature_alignment_loss: 0.002773, Cycle_consistency_loss: 0.002948, Scale_consensus_loss: 0.000000, Rot_MSE: 18.515181, Rot_RMSE: 4.302927, Rot_MAE: 2.554115, Rot_R2: 0.890538, Trans_MSE: 0.001287, Trans_RMSE: 0.035872, Trans_MAE: 0.023868, Trans_R2: 0.984501 +A->B:: Stage: test, Epoch: 39, Loss: 0.037156, Feature_alignment_loss: 0.002638, Cycle_consistency_loss: 0.002148, Scale_consensus_loss: 0.000000, Rot_MSE: 29.416401, Rot_RMSE: 5.423689, Rot_MAE: 2.915384, Rot_R2: 0.824946, Trans_MSE: 0.001402, Trans_RMSE: 0.037440, Trans_MAE: 0.024990, Trans_R2: 0.982983 +A->B:: Stage: best_test, Epoch: 38, Loss: 0.036186, Feature_alignment_loss: 0.002648, Cycle_consistency_loss: 0.002137, Scale_consensus_loss: 0.000000, Rot_MSE: 29.812449, Rot_RMSE: 5.460078, Rot_MAE: 2.936675, Rot_R2: 0.822578, Trans_MSE: 0.001279, Trans_RMSE: 0.035757, Trans_MAE: 0.023811, Trans_R2: 0.984467 +A->B:: Stage: train, Epoch: 40, Loss: 0.018569, Feature_alignment_loss: 0.002730, Cycle_consistency_loss: 0.002954, Scale_consensus_loss: 0.000000, Rot_MSE: 18.640421, Rot_RMSE: 4.317455, Rot_MAE: 2.534739, Rot_R2: 0.889758, Trans_MSE: 0.001260, Trans_RMSE: 0.035494, Trans_MAE: 0.023626, Trans_R2: 0.984824 +A->B:: Stage: test, Epoch: 40, Loss: 0.036961, Feature_alignment_loss: 0.002625, Cycle_consistency_loss: 0.002179, Scale_consensus_loss: 0.000000, Rot_MSE: 30.658789, Rot_RMSE: 5.537038, Rot_MAE: 2.961619, Rot_R2: 0.817581, Trans_MSE: 0.001440, Trans_RMSE: 0.037948, Trans_MAE: 0.025021, Trans_R2: 0.982509 +A->B:: Stage: best_test, Epoch: 38, Loss: 0.036186, Feature_alignment_loss: 0.002648, Cycle_consistency_loss: 0.002137, Scale_consensus_loss: 0.000000, Rot_MSE: 29.812449, Rot_RMSE: 5.460078, Rot_MAE: 2.936675, Rot_R2: 0.822578, Trans_MSE: 0.001279, Trans_RMSE: 0.035757, Trans_MAE: 0.023811, Trans_R2: 0.984467 +A->B:: Stage: train, Epoch: 41, Loss: 0.018403, Feature_alignment_loss: 0.002704, Cycle_consistency_loss: 0.002907, Scale_consensus_loss: 0.000000, Rot_MSE: 18.076120, Rot_RMSE: 4.251602, Rot_MAE: 2.534887, Rot_R2: 0.893164, Trans_MSE: 0.001267, Trans_RMSE: 0.035597, Trans_MAE: 0.023580, Trans_R2: 0.984737 +A->B:: Stage: test, Epoch: 41, Loss: 0.037019, Feature_alignment_loss: 0.002704, Cycle_consistency_loss: 0.002167, Scale_consensus_loss: 0.000000, Rot_MSE: 30.229744, Rot_RMSE: 5.498158, Rot_MAE: 2.928113, Rot_R2: 0.820158, Trans_MSE: 0.001421, Trans_RMSE: 0.037701, Trans_MAE: 0.024949, Trans_R2: 0.982752 +A->B:: Stage: best_test, Epoch: 38, Loss: 0.036186, Feature_alignment_loss: 0.002648, Cycle_consistency_loss: 0.002137, Scale_consensus_loss: 0.000000, Rot_MSE: 29.812449, Rot_RMSE: 5.460078, Rot_MAE: 2.936675, Rot_R2: 0.822578, Trans_MSE: 0.001279, Trans_RMSE: 0.035757, Trans_MAE: 0.023811, Trans_R2: 0.984467 +A->B:: Stage: train, Epoch: 42, Loss: 0.017950, Feature_alignment_loss: 0.002721, Cycle_consistency_loss: 0.002903, Scale_consensus_loss: 0.000000, Rot_MSE: 17.023247, Rot_RMSE: 4.125924, Rot_MAE: 2.492204, Rot_R2: 0.899377, Trans_MSE: 0.001250, Trans_RMSE: 0.035356, Trans_MAE: 0.023468, Trans_R2: 0.984942 +A->B:: Stage: test, Epoch: 42, Loss: 0.036309, Feature_alignment_loss: 0.002655, Cycle_consistency_loss: 0.002189, Scale_consensus_loss: 0.000000, Rot_MSE: 30.987963, Rot_RMSE: 5.566683, Rot_MAE: 2.961171, Rot_R2: 0.815656, Trans_MSE: 0.001435, Trans_RMSE: 0.037880, Trans_MAE: 0.025358, Trans_R2: 0.982561 +A->B:: Stage: best_test, Epoch: 38, Loss: 0.036186, Feature_alignment_loss: 0.002648, Cycle_consistency_loss: 0.002137, Scale_consensus_loss: 0.000000, Rot_MSE: 29.812449, Rot_RMSE: 5.460078, Rot_MAE: 2.936675, Rot_R2: 0.822578, Trans_MSE: 0.001279, Trans_RMSE: 0.035757, Trans_MAE: 0.023811, Trans_R2: 0.984467 +A->B:: Stage: train, Epoch: 43, Loss: 0.017829, Feature_alignment_loss: 0.002673, Cycle_consistency_loss: 0.002887, Scale_consensus_loss: 0.000000, Rot_MSE: 17.275143, Rot_RMSE: 4.156338, Rot_MAE: 2.499819, Rot_R2: 0.897909, Trans_MSE: 0.001213, Trans_RMSE: 0.034828, Trans_MAE: 0.023150, Trans_R2: 0.985390 +A->B:: Stage: test, Epoch: 43, Loss: 0.037159, Feature_alignment_loss: 0.002625, Cycle_consistency_loss: 0.002155, Scale_consensus_loss: 0.000000, Rot_MSE: 31.488453, Rot_RMSE: 5.611457, Rot_MAE: 2.933385, Rot_R2: 0.812636, Trans_MSE: 0.001441, Trans_RMSE: 0.037959, Trans_MAE: 0.025329, Trans_R2: 0.982496 +A->B:: Stage: best_test, Epoch: 38, Loss: 0.036186, Feature_alignment_loss: 0.002648, Cycle_consistency_loss: 0.002137, Scale_consensus_loss: 0.000000, Rot_MSE: 29.812449, Rot_RMSE: 5.460078, Rot_MAE: 2.936675, Rot_R2: 0.822578, Trans_MSE: 0.001279, Trans_RMSE: 0.035757, Trans_MAE: 0.023811, Trans_R2: 0.984467 +A->B:: Stage: train, Epoch: 44, Loss: 0.017761, Feature_alignment_loss: 0.002674, Cycle_consistency_loss: 0.002860, Scale_consensus_loss: 0.000000, Rot_MSE: 16.977325, Rot_RMSE: 4.120355, Rot_MAE: 2.489066, Rot_R2: 0.899670, Trans_MSE: 0.001230, Trans_RMSE: 0.035070, Trans_MAE: 0.023218, Trans_R2: 0.985186 +A->B:: Stage: test, Epoch: 44, Loss: 0.037020, Feature_alignment_loss: 0.002693, Cycle_consistency_loss: 0.002237, Scale_consensus_loss: 0.000000, Rot_MSE: 31.200014, Rot_RMSE: 5.585697, Rot_MAE: 2.963537, Rot_R2: 0.814358, Trans_MSE: 0.001198, Trans_RMSE: 0.034607, Trans_MAE: 0.023010, Trans_R2: 0.985448 +A->B:: Stage: best_test, Epoch: 38, Loss: 0.036186, Feature_alignment_loss: 0.002648, Cycle_consistency_loss: 0.002137, Scale_consensus_loss: 0.000000, Rot_MSE: 29.812449, Rot_RMSE: 5.460078, Rot_MAE: 2.936675, Rot_R2: 0.822578, Trans_MSE: 0.001279, Trans_RMSE: 0.035757, Trans_MAE: 0.023811, Trans_R2: 0.984467 +A->B:: Stage: train, Epoch: 45, Loss: 0.018052, Feature_alignment_loss: 0.002661, Cycle_consistency_loss: 0.002856, Scale_consensus_loss: 0.000000, Rot_MSE: 17.850018, Rot_RMSE: 4.224928, Rot_MAE: 2.503786, Rot_R2: 0.894501, Trans_MSE: 0.001244, Trans_RMSE: 0.035276, Trans_MAE: 0.023425, Trans_R2: 0.985008 +A->B:: Stage: test, Epoch: 45, Loss: 0.037492, Feature_alignment_loss: 0.002705, Cycle_consistency_loss: 0.002175, Scale_consensus_loss: 0.000000, Rot_MSE: 31.824512, Rot_RMSE: 5.641322, Rot_MAE: 2.979589, Rot_R2: 0.810654, Trans_MSE: 0.001267, Trans_RMSE: 0.035596, Trans_MAE: 0.023803, Trans_R2: 0.984592 +A->B:: Stage: best_test, Epoch: 38, Loss: 0.036186, Feature_alignment_loss: 0.002648, Cycle_consistency_loss: 0.002137, Scale_consensus_loss: 0.000000, Rot_MSE: 29.812449, Rot_RMSE: 5.460078, Rot_MAE: 2.936675, Rot_R2: 0.822578, Trans_MSE: 0.001279, Trans_RMSE: 0.035757, Trans_MAE: 0.023811, Trans_R2: 0.984467 +A->B:: Stage: train, Epoch: 46, Loss: 0.017706, Feature_alignment_loss: 0.002665, Cycle_consistency_loss: 0.002855, Scale_consensus_loss: 0.000000, Rot_MSE: 17.162298, Rot_RMSE: 4.142740, Rot_MAE: 2.472600, Rot_R2: 0.898541, Trans_MSE: 0.001187, Trans_RMSE: 0.034448, Trans_MAE: 0.022673, Trans_R2: 0.985706 +A->B:: Stage: test, Epoch: 46, Loss: 0.037864, Feature_alignment_loss: 0.002605, Cycle_consistency_loss: 0.002165, Scale_consensus_loss: 0.000000, Rot_MSE: 31.107046, Rot_RMSE: 5.577369, Rot_MAE: 2.921027, Rot_R2: 0.814900, Trans_MSE: 0.001457, Trans_RMSE: 0.038172, Trans_MAE: 0.025697, Trans_R2: 0.982304 +A->B:: Stage: best_test, Epoch: 38, Loss: 0.036186, Feature_alignment_loss: 0.002648, Cycle_consistency_loss: 0.002137, Scale_consensus_loss: 0.000000, Rot_MSE: 29.812449, Rot_RMSE: 5.460078, Rot_MAE: 2.936675, Rot_R2: 0.822578, Trans_MSE: 0.001279, Trans_RMSE: 0.035757, Trans_MAE: 0.023811, Trans_R2: 0.984467 +A->B:: Stage: train, Epoch: 47, Loss: 0.017642, Feature_alignment_loss: 0.002675, Cycle_consistency_loss: 0.002839, Scale_consensus_loss: 0.000000, Rot_MSE: 17.111380, Rot_RMSE: 4.136590, Rot_MAE: 2.490956, Rot_R2: 0.898905, Trans_MSE: 0.001181, Trans_RMSE: 0.034364, Trans_MAE: 0.022980, Trans_R2: 0.985777 +A->B:: Stage: test, Epoch: 47, Loss: 0.038214, Feature_alignment_loss: 0.002637, Cycle_consistency_loss: 0.002180, Scale_consensus_loss: 0.000000, Rot_MSE: 29.775101, Rot_RMSE: 5.456656, Rot_MAE: 2.904398, Rot_R2: 0.822850, Trans_MSE: 0.001333, Trans_RMSE: 0.036506, Trans_MAE: 0.024348, Trans_R2: 0.983824 +A->B:: Stage: best_test, Epoch: 38, Loss: 0.036186, Feature_alignment_loss: 0.002648, Cycle_consistency_loss: 0.002137, Scale_consensus_loss: 0.000000, Rot_MSE: 29.812449, Rot_RMSE: 5.460078, Rot_MAE: 2.936675, Rot_R2: 0.822578, Trans_MSE: 0.001279, Trans_RMSE: 0.035757, Trans_MAE: 0.023811, Trans_R2: 0.984467 +A->B:: Stage: train, Epoch: 48, Loss: 0.017635, Feature_alignment_loss: 0.002666, Cycle_consistency_loss: 0.002796, Scale_consensus_loss: 0.000000, Rot_MSE: 17.273729, Rot_RMSE: 4.156168, Rot_MAE: 2.485117, Rot_R2: 0.897852, Trans_MSE: 0.001201, Trans_RMSE: 0.034658, Trans_MAE: 0.022976, Trans_R2: 0.985532 +A->B:: Stage: test, Epoch: 48, Loss: 0.039776, Feature_alignment_loss: 0.002583, Cycle_consistency_loss: 0.002120, Scale_consensus_loss: 0.000000, Rot_MSE: 30.732752, Rot_RMSE: 5.543713, Rot_MAE: 2.899945, Rot_R2: 0.817080, Trans_MSE: 0.001579, Trans_RMSE: 0.039737, Trans_MAE: 0.026904, Trans_R2: 0.980834 +A->B:: Stage: best_test, Epoch: 38, Loss: 0.036186, Feature_alignment_loss: 0.002648, Cycle_consistency_loss: 0.002137, Scale_consensus_loss: 0.000000, Rot_MSE: 29.812449, Rot_RMSE: 5.460078, Rot_MAE: 2.936675, Rot_R2: 0.822578, Trans_MSE: 0.001279, Trans_RMSE: 0.035757, Trans_MAE: 0.023811, Trans_R2: 0.984467 +A->B:: Stage: train, Epoch: 49, Loss: 0.017653, Feature_alignment_loss: 0.002679, Cycle_consistency_loss: 0.002819, Scale_consensus_loss: 0.000000, Rot_MSE: 17.389000, Rot_RMSE: 4.170012, Rot_MAE: 2.486597, Rot_R2: 0.897264, Trans_MSE: 0.001169, Trans_RMSE: 0.034188, Trans_MAE: 0.022650, Trans_R2: 0.985920 +A->B:: Stage: test, Epoch: 49, Loss: 0.038529, Feature_alignment_loss: 0.002627, Cycle_consistency_loss: 0.002166, Scale_consensus_loss: 0.000000, Rot_MSE: 31.844206, Rot_RMSE: 5.643067, Rot_MAE: 2.956324, Rot_R2: 0.810548, Trans_MSE: 0.001219, Trans_RMSE: 0.034915, Trans_MAE: 0.023470, Trans_R2: 0.985184 +A->B:: Stage: best_test, Epoch: 38, Loss: 0.036186, Feature_alignment_loss: 0.002648, Cycle_consistency_loss: 0.002137, Scale_consensus_loss: 0.000000, Rot_MSE: 29.812449, Rot_RMSE: 5.460078, Rot_MAE: 2.936675, Rot_R2: 0.822578, Trans_MSE: 0.001279, Trans_RMSE: 0.035757, Trans_MAE: 0.023811, Trans_R2: 0.984467 +A->B:: Stage: train, Epoch: 50, Loss: 0.017221, Feature_alignment_loss: 0.002634, Cycle_consistency_loss: 0.002783, Scale_consensus_loss: 0.000000, Rot_MSE: 16.531504, Rot_RMSE: 4.065895, Rot_MAE: 2.451247, Rot_R2: 0.902309, Trans_MSE: 0.001146, Trans_RMSE: 0.033853, Trans_MAE: 0.022508, Trans_R2: 0.986195 +A->B:: Stage: test, Epoch: 50, Loss: 0.037641, Feature_alignment_loss: 0.002625, Cycle_consistency_loss: 0.002139, Scale_consensus_loss: 0.000000, Rot_MSE: 29.589926, Rot_RMSE: 5.439662, Rot_MAE: 2.883605, Rot_R2: 0.823925, Trans_MSE: 0.001272, Trans_RMSE: 0.035663, Trans_MAE: 0.024109, Trans_R2: 0.984566 +A->B:: Stage: best_test, Epoch: 38, Loss: 0.036186, Feature_alignment_loss: 0.002648, Cycle_consistency_loss: 0.002137, Scale_consensus_loss: 0.000000, Rot_MSE: 29.812449, Rot_RMSE: 5.460078, Rot_MAE: 2.936675, Rot_R2: 0.822578, Trans_MSE: 0.001279, Trans_RMSE: 0.035757, Trans_MAE: 0.023811, Trans_R2: 0.984467 +A->B:: Stage: train, Epoch: 51, Loss: 0.017651, Feature_alignment_loss: 0.002634, Cycle_consistency_loss: 0.002810, Scale_consensus_loss: 0.000000, Rot_MSE: 17.718132, Rot_RMSE: 4.209291, Rot_MAE: 2.480686, Rot_R2: 0.895266, Trans_MSE: 0.001164, Trans_RMSE: 0.034112, Trans_MAE: 0.022610, Trans_R2: 0.985985 +A->B:: Stage: test, Epoch: 51, Loss: 0.035683, Feature_alignment_loss: 0.002591, Cycle_consistency_loss: 0.002204, Scale_consensus_loss: 0.000000, Rot_MSE: 29.584118, Rot_RMSE: 5.439128, Rot_MAE: 2.855554, Rot_R2: 0.823903, Trans_MSE: 0.001184, Trans_RMSE: 0.034415, Trans_MAE: 0.023091, Trans_R2: 0.985623 +A->B:: Stage: best_test, Epoch: 51, Loss: 0.035683, Feature_alignment_loss: 0.002591, Cycle_consistency_loss: 0.002204, Scale_consensus_loss: 0.000000, Rot_MSE: 29.584118, Rot_RMSE: 5.439128, Rot_MAE: 2.855554, Rot_R2: 0.823903, Trans_MSE: 0.001184, Trans_RMSE: 0.034415, Trans_MAE: 0.023091, Trans_R2: 0.985623 +A->B:: Stage: train, Epoch: 52, Loss: 0.017586, Feature_alignment_loss: 0.002631, Cycle_consistency_loss: 0.002803, Scale_consensus_loss: 0.000000, Rot_MSE: 17.684288, Rot_RMSE: 4.205269, Rot_MAE: 2.487306, Rot_R2: 0.895448, Trans_MSE: 0.001124, Trans_RMSE: 0.033530, Trans_MAE: 0.022314, Trans_R2: 0.986457 +A->B:: Stage: test, Epoch: 52, Loss: 0.037029, Feature_alignment_loss: 0.002611, Cycle_consistency_loss: 0.002165, Scale_consensus_loss: 0.000000, Rot_MSE: 30.672707, Rot_RMSE: 5.538295, Rot_MAE: 2.905256, Rot_R2: 0.817441, Trans_MSE: 0.001400, Trans_RMSE: 0.037421, Trans_MAE: 0.025069, Trans_R2: 0.982972 +A->B:: Stage: best_test, Epoch: 51, Loss: 0.035683, Feature_alignment_loss: 0.002591, Cycle_consistency_loss: 0.002204, Scale_consensus_loss: 0.000000, Rot_MSE: 29.584118, Rot_RMSE: 5.439128, Rot_MAE: 2.855554, Rot_R2: 0.823903, Trans_MSE: 0.001184, Trans_RMSE: 0.034415, Trans_MAE: 0.023091, Trans_R2: 0.985623 +A->B:: Stage: train, Epoch: 53, Loss: 0.017325, Feature_alignment_loss: 0.002588, Cycle_consistency_loss: 0.002772, Scale_consensus_loss: 0.000000, Rot_MSE: 16.811907, Rot_RMSE: 4.100233, Rot_MAE: 2.436039, Rot_R2: 0.900684, Trans_MSE: 0.001182, Trans_RMSE: 0.034377, Trans_MAE: 0.022720, Trans_R2: 0.985765 +A->B:: Stage: test, Epoch: 53, Loss: 0.036299, Feature_alignment_loss: 0.002552, Cycle_consistency_loss: 0.002173, Scale_consensus_loss: 0.000000, Rot_MSE: 29.862202, Rot_RMSE: 5.464632, Rot_MAE: 2.850914, Rot_R2: 0.822272, Trans_MSE: 0.001345, Trans_RMSE: 0.036668, Trans_MAE: 0.024641, Trans_R2: 0.983662 +A->B:: Stage: best_test, Epoch: 51, Loss: 0.035683, Feature_alignment_loss: 0.002591, Cycle_consistency_loss: 0.002204, Scale_consensus_loss: 0.000000, Rot_MSE: 29.584118, Rot_RMSE: 5.439128, Rot_MAE: 2.855554, Rot_R2: 0.823903, Trans_MSE: 0.001184, Trans_RMSE: 0.034415, Trans_MAE: 0.023091, Trans_R2: 0.985623 +A->B:: Stage: train, Epoch: 54, Loss: 0.017212, Feature_alignment_loss: 0.002620, Cycle_consistency_loss: 0.002762, Scale_consensus_loss: 0.000000, Rot_MSE: 16.416563, Rot_RMSE: 4.051736, Rot_MAE: 2.421885, Rot_R2: 0.902969, Trans_MSE: 0.001156, Trans_RMSE: 0.033995, Trans_MAE: 0.022501, Trans_R2: 0.986081 +A->B:: Stage: test, Epoch: 54, Loss: 0.037839, Feature_alignment_loss: 0.002592, Cycle_consistency_loss: 0.002131, Scale_consensus_loss: 0.000000, Rot_MSE: 30.304634, Rot_RMSE: 5.504964, Rot_MAE: 2.888907, Rot_R2: 0.819643, Trans_MSE: 0.001305, Trans_RMSE: 0.036131, Trans_MAE: 0.024096, Trans_R2: 0.984143 +A->B:: Stage: best_test, Epoch: 51, Loss: 0.035683, Feature_alignment_loss: 0.002591, Cycle_consistency_loss: 0.002204, Scale_consensus_loss: 0.000000, Rot_MSE: 29.584118, Rot_RMSE: 5.439128, Rot_MAE: 2.855554, Rot_R2: 0.823903, Trans_MSE: 0.001184, Trans_RMSE: 0.034415, Trans_MAE: 0.023091, Trans_R2: 0.985623 +A->B:: Stage: train, Epoch: 55, Loss: 0.017358, Feature_alignment_loss: 0.002620, Cycle_consistency_loss: 0.002740, Scale_consensus_loss: 0.000000, Rot_MSE: 16.812624, Rot_RMSE: 4.100320, Rot_MAE: 2.453818, Rot_R2: 0.900643, Trans_MSE: 0.001137, Trans_RMSE: 0.033713, Trans_MAE: 0.022130, Trans_R2: 0.986308 +A->B:: Stage: test, Epoch: 55, Loss: 0.037609, Feature_alignment_loss: 0.002595, Cycle_consistency_loss: 0.002112, Scale_consensus_loss: 0.000000, Rot_MSE: 30.868826, Rot_RMSE: 5.555972, Rot_MAE: 2.906902, Rot_R2: 0.816333, Trans_MSE: 0.001248, Trans_RMSE: 0.035323, Trans_MAE: 0.023570, Trans_R2: 0.984867 +A->B:: Stage: best_test, Epoch: 51, Loss: 0.035683, Feature_alignment_loss: 0.002591, Cycle_consistency_loss: 0.002204, Scale_consensus_loss: 0.000000, Rot_MSE: 29.584118, Rot_RMSE: 5.439128, Rot_MAE: 2.855554, Rot_R2: 0.823903, Trans_MSE: 0.001184, Trans_RMSE: 0.034415, Trans_MAE: 0.023091, Trans_R2: 0.985623 +A->B:: Stage: train, Epoch: 56, Loss: 0.016869, Feature_alignment_loss: 0.002573, Cycle_consistency_loss: 0.002741, Scale_consensus_loss: 0.000000, Rot_MSE: 16.077639, Rot_RMSE: 4.009693, Rot_MAE: 2.413860, Rot_R2: 0.904947, Trans_MSE: 0.001106, Trans_RMSE: 0.033263, Trans_MAE: 0.022055, Trans_R2: 0.986672 +A->B:: Stage: test, Epoch: 56, Loss: 0.036169, Feature_alignment_loss: 0.002450, Cycle_consistency_loss: 0.002134, Scale_consensus_loss: 0.000000, Rot_MSE: 28.829334, Rot_RMSE: 5.369296, Rot_MAE: 2.842875, Rot_R2: 0.828438, Trans_MSE: 0.001254, Trans_RMSE: 0.035418, Trans_MAE: 0.023613, Trans_R2: 0.984765 +A->B:: Stage: best_test, Epoch: 51, Loss: 0.035683, Feature_alignment_loss: 0.002591, Cycle_consistency_loss: 0.002204, Scale_consensus_loss: 0.000000, Rot_MSE: 29.584118, Rot_RMSE: 5.439128, Rot_MAE: 2.855554, Rot_R2: 0.823903, Trans_MSE: 0.001184, Trans_RMSE: 0.034415, Trans_MAE: 0.023091, Trans_R2: 0.985623 +A->B:: Stage: train, Epoch: 57, Loss: 0.017074, Feature_alignment_loss: 0.002571, Cycle_consistency_loss: 0.002741, Scale_consensus_loss: 0.000000, Rot_MSE: 16.358284, Rot_RMSE: 4.044538, Rot_MAE: 2.432117, Rot_R2: 0.903346, Trans_MSE: 0.001095, Trans_RMSE: 0.033084, Trans_MAE: 0.022013, Trans_R2: 0.986814 +A->B:: Stage: test, Epoch: 57, Loss: 0.036816, Feature_alignment_loss: 0.002601, Cycle_consistency_loss: 0.002124, Scale_consensus_loss: 0.000000, Rot_MSE: 29.067886, Rot_RMSE: 5.391464, Rot_MAE: 2.838008, Rot_R2: 0.827031, Trans_MSE: 0.001223, Trans_RMSE: 0.034973, Trans_MAE: 0.023244, Trans_R2: 0.985163 +A->B:: Stage: best_test, Epoch: 51, Loss: 0.035683, Feature_alignment_loss: 0.002591, Cycle_consistency_loss: 0.002204, Scale_consensus_loss: 0.000000, Rot_MSE: 29.584118, Rot_RMSE: 5.439128, Rot_MAE: 2.855554, Rot_R2: 0.823903, Trans_MSE: 0.001184, Trans_RMSE: 0.034415, Trans_MAE: 0.023091, Trans_R2: 0.985623 +A->B:: Stage: train, Epoch: 58, Loss: 0.017009, Feature_alignment_loss: 0.002568, Cycle_consistency_loss: 0.002752, Scale_consensus_loss: 0.000000, Rot_MSE: 16.354559, Rot_RMSE: 4.044077, Rot_MAE: 2.420130, Rot_R2: 0.903371, Trans_MSE: 0.001098, Trans_RMSE: 0.033138, Trans_MAE: 0.021977, Trans_R2: 0.986773 +A->B:: Stage: test, Epoch: 58, Loss: 0.036235, Feature_alignment_loss: 0.002548, Cycle_consistency_loss: 0.002140, Scale_consensus_loss: 0.000000, Rot_MSE: 28.315592, Rot_RMSE: 5.321239, Rot_MAE: 2.831649, Rot_R2: 0.831506, Trans_MSE: 0.001149, Trans_RMSE: 0.033893, Trans_MAE: 0.022325, Trans_R2: 0.986057 +A->B:: Stage: best_test, Epoch: 51, Loss: 0.035683, Feature_alignment_loss: 0.002591, Cycle_consistency_loss: 0.002204, Scale_consensus_loss: 0.000000, Rot_MSE: 29.584118, Rot_RMSE: 5.439128, Rot_MAE: 2.855554, Rot_R2: 0.823903, Trans_MSE: 0.001184, Trans_RMSE: 0.034415, Trans_MAE: 0.023091, Trans_R2: 0.985623 +A->B:: Stage: train, Epoch: 59, Loss: 0.016979, Feature_alignment_loss: 0.002593, Cycle_consistency_loss: 0.002750, Scale_consensus_loss: 0.000000, Rot_MSE: 16.476768, Rot_RMSE: 4.059159, Rot_MAE: 2.410615, Rot_R2: 0.902602, Trans_MSE: 0.001083, Trans_RMSE: 0.032911, Trans_MAE: 0.021880, Trans_R2: 0.986952 +A->B:: Stage: test, Epoch: 59, Loss: 0.036959, Feature_alignment_loss: 0.002507, Cycle_consistency_loss: 0.002142, Scale_consensus_loss: 0.000000, Rot_MSE: 29.544245, Rot_RMSE: 5.435462, Rot_MAE: 2.835836, Rot_R2: 0.824182, Trans_MSE: 0.001233, Trans_RMSE: 0.035110, Trans_MAE: 0.023243, Trans_R2: 0.985038 +A->B:: Stage: best_test, Epoch: 51, Loss: 0.035683, Feature_alignment_loss: 0.002591, Cycle_consistency_loss: 0.002204, Scale_consensus_loss: 0.000000, Rot_MSE: 29.584118, Rot_RMSE: 5.439128, Rot_MAE: 2.855554, Rot_R2: 0.823903, Trans_MSE: 0.001184, Trans_RMSE: 0.034415, Trans_MAE: 0.023091, Trans_R2: 0.985623 +A->B:: Stage: train, Epoch: 60, Loss: 0.016591, Feature_alignment_loss: 0.002504, Cycle_consistency_loss: 0.002701, Scale_consensus_loss: 0.000000, Rot_MSE: 16.427208, Rot_RMSE: 4.053049, Rot_MAE: 2.410421, Rot_R2: 0.902953, Trans_MSE: 0.001055, Trans_RMSE: 0.032486, Trans_MAE: 0.021478, Trans_R2: 0.987288 +A->B:: Stage: test, Epoch: 60, Loss: 0.035142, Feature_alignment_loss: 0.002513, Cycle_consistency_loss: 0.002158, Scale_consensus_loss: 0.000000, Rot_MSE: 28.943426, Rot_RMSE: 5.379910, Rot_MAE: 2.790904, Rot_R2: 0.827727, Trans_MSE: 0.001164, Trans_RMSE: 0.034122, Trans_MAE: 0.022320, Trans_R2: 0.985865 +A->B:: Stage: best_test, Epoch: 60, Loss: 0.035142, Feature_alignment_loss: 0.002513, Cycle_consistency_loss: 0.002158, Scale_consensus_loss: 0.000000, Rot_MSE: 28.943426, Rot_RMSE: 5.379910, Rot_MAE: 2.790904, Rot_R2: 0.827727, Trans_MSE: 0.001164, Trans_RMSE: 0.034122, Trans_MAE: 0.022320, Trans_R2: 0.985865 +A->B:: Stage: train, Epoch: 61, Loss: 0.016581, Feature_alignment_loss: 0.002499, Cycle_consistency_loss: 0.002707, Scale_consensus_loss: 0.000000, Rot_MSE: 15.846601, Rot_RMSE: 3.980779, Rot_MAE: 2.368487, Rot_R2: 0.906385, Trans_MSE: 0.001098, Trans_RMSE: 0.033137, Trans_MAE: 0.021698, Trans_R2: 0.986772 +A->B:: Stage: test, Epoch: 61, Loss: 0.036158, Feature_alignment_loss: 0.002511, Cycle_consistency_loss: 0.002149, Scale_consensus_loss: 0.000000, Rot_MSE: 29.848438, Rot_RMSE: 5.463372, Rot_MAE: 2.814405, Rot_R2: 0.822344, Trans_MSE: 0.001189, Trans_RMSE: 0.034480, Trans_MAE: 0.022723, Trans_R2: 0.985565 +A->B:: Stage: best_test, Epoch: 60, Loss: 0.035142, Feature_alignment_loss: 0.002513, Cycle_consistency_loss: 0.002158, Scale_consensus_loss: 0.000000, Rot_MSE: 28.943426, Rot_RMSE: 5.379910, Rot_MAE: 2.790904, Rot_R2: 0.827727, Trans_MSE: 0.001164, Trans_RMSE: 0.034122, Trans_MAE: 0.022320, Trans_R2: 0.985865 +A->B:: Stage: train, Epoch: 62, Loss: 0.016536, Feature_alignment_loss: 0.002512, Cycle_consistency_loss: 0.002697, Scale_consensus_loss: 0.000000, Rot_MSE: 16.185633, Rot_RMSE: 4.023137, Rot_MAE: 2.399647, Rot_R2: 0.904334, Trans_MSE: 0.001070, Trans_RMSE: 0.032706, Trans_MAE: 0.021471, Trans_R2: 0.987116 +A->B:: Stage: test, Epoch: 62, Loss: 0.034509, Feature_alignment_loss: 0.002482, Cycle_consistency_loss: 0.002146, Scale_consensus_loss: 0.000000, Rot_MSE: 28.292456, Rot_RMSE: 5.319065, Rot_MAE: 2.745904, Rot_R2: 0.831611, Trans_MSE: 0.001165, Trans_RMSE: 0.034132, Trans_MAE: 0.022579, Trans_R2: 0.985856 +A->B:: Stage: best_test, Epoch: 62, Loss: 0.034509, Feature_alignment_loss: 0.002482, Cycle_consistency_loss: 0.002146, Scale_consensus_loss: 0.000000, Rot_MSE: 28.292456, Rot_RMSE: 5.319065, Rot_MAE: 2.745904, Rot_R2: 0.831611, Trans_MSE: 0.001165, Trans_RMSE: 0.034132, Trans_MAE: 0.022579, Trans_R2: 0.985856 +A->B:: Stage: train, Epoch: 63, Loss: 0.016259, Feature_alignment_loss: 0.002493, Cycle_consistency_loss: 0.002697, Scale_consensus_loss: 0.000000, Rot_MSE: 15.612621, Rot_RMSE: 3.951281, Rot_MAE: 2.361468, Rot_R2: 0.907687, Trans_MSE: 0.001060, Trans_RMSE: 0.032553, Trans_MAE: 0.021512, Trans_R2: 0.987235 +A->B:: Stage: test, Epoch: 63, Loss: 0.035974, Feature_alignment_loss: 0.002521, Cycle_consistency_loss: 0.002187, Scale_consensus_loss: 0.000000, Rot_MSE: 29.587410, Rot_RMSE: 5.439431, Rot_MAE: 2.813848, Rot_R2: 0.823914, Trans_MSE: 0.001271, Trans_RMSE: 0.035650, Trans_MAE: 0.023591, Trans_R2: 0.984566 +A->B:: Stage: best_test, Epoch: 62, Loss: 0.034509, Feature_alignment_loss: 0.002482, Cycle_consistency_loss: 0.002146, Scale_consensus_loss: 0.000000, Rot_MSE: 28.292456, Rot_RMSE: 5.319065, Rot_MAE: 2.745904, Rot_R2: 0.831611, Trans_MSE: 0.001165, Trans_RMSE: 0.034132, Trans_MAE: 0.022579, Trans_R2: 0.985856 +A->B:: Stage: train, Epoch: 64, Loss: 0.016224, Feature_alignment_loss: 0.002507, Cycle_consistency_loss: 0.002688, Scale_consensus_loss: 0.000000, Rot_MSE: 15.451701, Rot_RMSE: 3.930865, Rot_MAE: 2.348138, Rot_R2: 0.908681, Trans_MSE: 0.001055, Trans_RMSE: 0.032482, Trans_MAE: 0.021358, Trans_R2: 0.987290 +A->B:: Stage: test, Epoch: 64, Loss: 0.036538, Feature_alignment_loss: 0.002505, Cycle_consistency_loss: 0.002136, Scale_consensus_loss: 0.000000, Rot_MSE: 29.650721, Rot_RMSE: 5.445248, Rot_MAE: 2.810968, Rot_R2: 0.823533, Trans_MSE: 0.001188, Trans_RMSE: 0.034472, Trans_MAE: 0.022774, Trans_R2: 0.985569 +A->B:: Stage: best_test, Epoch: 62, Loss: 0.034509, Feature_alignment_loss: 0.002482, Cycle_consistency_loss: 0.002146, Scale_consensus_loss: 0.000000, Rot_MSE: 28.292456, Rot_RMSE: 5.319065, Rot_MAE: 2.745904, Rot_R2: 0.831611, Trans_MSE: 0.001165, Trans_RMSE: 0.034132, Trans_MAE: 0.022579, Trans_R2: 0.985856 +A->B:: Stage: train, Epoch: 65, Loss: 0.016467, Feature_alignment_loss: 0.002510, Cycle_consistency_loss: 0.002691, Scale_consensus_loss: 0.000000, Rot_MSE: 15.815068, Rot_RMSE: 3.976816, Rot_MAE: 2.368981, Rot_R2: 0.906553, Trans_MSE: 0.001060, Trans_RMSE: 0.032555, Trans_MAE: 0.021559, Trans_R2: 0.987234 +A->B:: Stage: test, Epoch: 65, Loss: 0.036958, Feature_alignment_loss: 0.002569, Cycle_consistency_loss: 0.002171, Scale_consensus_loss: 0.000000, Rot_MSE: 29.884571, Rot_RMSE: 5.466678, Rot_MAE: 2.816575, Rot_R2: 0.822184, Trans_MSE: 0.001331, Trans_RMSE: 0.036487, Trans_MAE: 0.024266, Trans_R2: 0.983832 +A->B:: Stage: best_test, Epoch: 62, Loss: 0.034509, Feature_alignment_loss: 0.002482, Cycle_consistency_loss: 0.002146, Scale_consensus_loss: 0.000000, Rot_MSE: 28.292456, Rot_RMSE: 5.319065, Rot_MAE: 2.745904, Rot_R2: 0.831611, Trans_MSE: 0.001165, Trans_RMSE: 0.034132, Trans_MAE: 0.022579, Trans_R2: 0.985856 +A->B:: Stage: train, Epoch: 66, Loss: 0.016043, Feature_alignment_loss: 0.002507, Cycle_consistency_loss: 0.002671, Scale_consensus_loss: 0.000000, Rot_MSE: 14.974280, Rot_RMSE: 3.869662, Rot_MAE: 2.325844, Rot_R2: 0.911506, Trans_MSE: 0.001059, Trans_RMSE: 0.032548, Trans_MAE: 0.021301, Trans_R2: 0.987240 +A->B:: Stage: test, Epoch: 66, Loss: 0.034793, Feature_alignment_loss: 0.002503, Cycle_consistency_loss: 0.002144, Scale_consensus_loss: 0.000000, Rot_MSE: 28.232508, Rot_RMSE: 5.313427, Rot_MAE: 2.759583, Rot_R2: 0.831982, Trans_MSE: 0.001150, Trans_RMSE: 0.033906, Trans_MAE: 0.021999, Trans_R2: 0.986046 +A->B:: Stage: best_test, Epoch: 62, Loss: 0.034509, Feature_alignment_loss: 0.002482, Cycle_consistency_loss: 0.002146, Scale_consensus_loss: 0.000000, Rot_MSE: 28.292456, Rot_RMSE: 5.319065, Rot_MAE: 2.745904, Rot_R2: 0.831611, Trans_MSE: 0.001165, Trans_RMSE: 0.034132, Trans_MAE: 0.022579, Trans_R2: 0.985856 +A->B:: Stage: train, Epoch: 67, Loss: 0.016308, Feature_alignment_loss: 0.002507, Cycle_consistency_loss: 0.002686, Scale_consensus_loss: 0.000000, Rot_MSE: 15.481644, Rot_RMSE: 3.934672, Rot_MAE: 2.364791, Rot_R2: 0.908492, Trans_MSE: 0.001073, Trans_RMSE: 0.032756, Trans_MAE: 0.021325, Trans_R2: 0.987074 +A->B:: Stage: test, Epoch: 67, Loss: 0.034860, Feature_alignment_loss: 0.002513, Cycle_consistency_loss: 0.002133, Scale_consensus_loss: 0.000000, Rot_MSE: 28.504349, Rot_RMSE: 5.338946, Rot_MAE: 2.775354, Rot_R2: 0.830358, Trans_MSE: 0.001150, Trans_RMSE: 0.033906, Trans_MAE: 0.022078, Trans_R2: 0.986041 +A->B:: Stage: best_test, Epoch: 62, Loss: 0.034509, Feature_alignment_loss: 0.002482, Cycle_consistency_loss: 0.002146, Scale_consensus_loss: 0.000000, Rot_MSE: 28.292456, Rot_RMSE: 5.319065, Rot_MAE: 2.745904, Rot_R2: 0.831611, Trans_MSE: 0.001165, Trans_RMSE: 0.034132, Trans_MAE: 0.022579, Trans_R2: 0.985856 +A->B:: Stage: train, Epoch: 68, Loss: 0.016179, Feature_alignment_loss: 0.002495, Cycle_consistency_loss: 0.002690, Scale_consensus_loss: 0.000000, Rot_MSE: 15.282664, Rot_RMSE: 3.909305, Rot_MAE: 2.336464, Rot_R2: 0.909696, Trans_MSE: 0.001066, Trans_RMSE: 0.032651, Trans_MAE: 0.021340, Trans_R2: 0.987158 +A->B:: Stage: test, Epoch: 68, Loss: 0.034835, Feature_alignment_loss: 0.002474, Cycle_consistency_loss: 0.002140, Scale_consensus_loss: 0.000000, Rot_MSE: 28.850359, Rot_RMSE: 5.371253, Rot_MAE: 2.777618, Rot_R2: 0.828294, Trans_MSE: 0.001181, Trans_RMSE: 0.034367, Trans_MAE: 0.022476, Trans_R2: 0.985661 +A->B:: Stage: best_test, Epoch: 62, Loss: 0.034509, Feature_alignment_loss: 0.002482, Cycle_consistency_loss: 0.002146, Scale_consensus_loss: 0.000000, Rot_MSE: 28.292456, Rot_RMSE: 5.319065, Rot_MAE: 2.745904, Rot_R2: 0.831611, Trans_MSE: 0.001165, Trans_RMSE: 0.034132, Trans_MAE: 0.022579, Trans_R2: 0.985856 +A->B:: Stage: train, Epoch: 69, Loss: 0.016305, Feature_alignment_loss: 0.002484, Cycle_consistency_loss: 0.002683, Scale_consensus_loss: 0.000000, Rot_MSE: 15.588045, Rot_RMSE: 3.948170, Rot_MAE: 2.360842, Rot_R2: 0.907883, Trans_MSE: 0.001065, Trans_RMSE: 0.032629, Trans_MAE: 0.021519, Trans_R2: 0.987176 +A->B:: Stage: test, Epoch: 69, Loss: 0.034415, Feature_alignment_loss: 0.002544, Cycle_consistency_loss: 0.002177, Scale_consensus_loss: 0.000000, Rot_MSE: 28.990061, Rot_RMSE: 5.384242, Rot_MAE: 2.779793, Rot_R2: 0.827505, Trans_MSE: 0.001127, Trans_RMSE: 0.033575, Trans_MAE: 0.022135, Trans_R2: 0.986308 +A->B:: Stage: best_test, Epoch: 69, Loss: 0.034415, Feature_alignment_loss: 0.002544, Cycle_consistency_loss: 0.002177, Scale_consensus_loss: 0.000000, Rot_MSE: 28.990061, Rot_RMSE: 5.384242, Rot_MAE: 2.779793, Rot_R2: 0.827505, Trans_MSE: 0.001127, Trans_RMSE: 0.033575, Trans_MAE: 0.022135, Trans_R2: 0.986308 +A->B:: Stage: train, Epoch: 70, Loss: 0.016437, Feature_alignment_loss: 0.002513, Cycle_consistency_loss: 0.002683, Scale_consensus_loss: 0.000000, Rot_MSE: 15.877062, Rot_RMSE: 3.984603, Rot_MAE: 2.373026, Rot_R2: 0.906186, Trans_MSE: 0.001067, Trans_RMSE: 0.032662, Trans_MAE: 0.021453, Trans_R2: 0.987151 +A->B:: Stage: test, Epoch: 70, Loss: 0.034207, Feature_alignment_loss: 0.002514, Cycle_consistency_loss: 0.002155, Scale_consensus_loss: 0.000000, Rot_MSE: 28.522974, Rot_RMSE: 5.340691, Rot_MAE: 2.761719, Rot_R2: 0.830283, Trans_MSE: 0.001124, Trans_RMSE: 0.033520, Trans_MAE: 0.022000, Trans_R2: 0.986358 +A->B:: Stage: best_test, Epoch: 70, Loss: 0.034207, Feature_alignment_loss: 0.002514, Cycle_consistency_loss: 0.002155, Scale_consensus_loss: 0.000000, Rot_MSE: 28.522974, Rot_RMSE: 5.340691, Rot_MAE: 2.761719, Rot_R2: 0.830283, Trans_MSE: 0.001124, Trans_RMSE: 0.033520, Trans_MAE: 0.022000, Trans_R2: 0.986358 +A->B:: Stage: train, Epoch: 71, Loss: 0.016222, Feature_alignment_loss: 0.002497, Cycle_consistency_loss: 0.002671, Scale_consensus_loss: 0.000000, Rot_MSE: 15.457500, Rot_RMSE: 3.931603, Rot_MAE: 2.355347, Rot_R2: 0.908663, Trans_MSE: 0.001057, Trans_RMSE: 0.032514, Trans_MAE: 0.021475, Trans_R2: 0.987265 +A->B:: Stage: test, Epoch: 71, Loss: 0.034868, Feature_alignment_loss: 0.002494, Cycle_consistency_loss: 0.002146, Scale_consensus_loss: 0.000000, Rot_MSE: 28.805676, Rot_RMSE: 5.367092, Rot_MAE: 2.770252, Rot_R2: 0.828556, Trans_MSE: 0.001153, Trans_RMSE: 0.033949, Trans_MAE: 0.022355, Trans_R2: 0.986000 +A->B:: Stage: best_test, Epoch: 70, Loss: 0.034207, Feature_alignment_loss: 0.002514, Cycle_consistency_loss: 0.002155, Scale_consensus_loss: 0.000000, Rot_MSE: 28.522974, Rot_RMSE: 5.340691, Rot_MAE: 2.761719, Rot_R2: 0.830283, Trans_MSE: 0.001124, Trans_RMSE: 0.033520, Trans_MAE: 0.022000, Trans_R2: 0.986358 +A->B:: Stage: train, Epoch: 72, Loss: 0.016282, Feature_alignment_loss: 0.002495, Cycle_consistency_loss: 0.002673, Scale_consensus_loss: 0.000000, Rot_MSE: 15.471003, Rot_RMSE: 3.933320, Rot_MAE: 2.350660, Rot_R2: 0.908571, Trans_MSE: 0.001086, Trans_RMSE: 0.032954, Trans_MAE: 0.021533, Trans_R2: 0.986919 +A->B:: Stage: test, Epoch: 72, Loss: 0.036009, Feature_alignment_loss: 0.002461, Cycle_consistency_loss: 0.002126, Scale_consensus_loss: 0.000000, Rot_MSE: 29.340725, Rot_RMSE: 5.416708, Rot_MAE: 2.776518, Rot_R2: 0.825381, Trans_MSE: 0.001109, Trans_RMSE: 0.033301, Trans_MAE: 0.022010, Trans_R2: 0.986524 +A->B:: Stage: best_test, Epoch: 70, Loss: 0.034207, Feature_alignment_loss: 0.002514, Cycle_consistency_loss: 0.002155, Scale_consensus_loss: 0.000000, Rot_MSE: 28.522974, Rot_RMSE: 5.340691, Rot_MAE: 2.761719, Rot_R2: 0.830283, Trans_MSE: 0.001124, Trans_RMSE: 0.033520, Trans_MAE: 0.022000, Trans_R2: 0.986358 +A->B:: Stage: train, Epoch: 73, Loss: 0.016236, Feature_alignment_loss: 0.002503, Cycle_consistency_loss: 0.002644, Scale_consensus_loss: 0.000000, Rot_MSE: 17.145651, Rot_RMSE: 4.140731, Rot_MAE: 2.338114, Rot_R2: 0.898551, Trans_MSE: 0.001030, Trans_RMSE: 0.032099, Trans_MAE: 0.021317, Trans_R2: 0.987588 +A->B:: Stage: test, Epoch: 73, Loss: 0.034615, Feature_alignment_loss: 0.002478, Cycle_consistency_loss: 0.002143, Scale_consensus_loss: 0.000000, Rot_MSE: 28.557766, Rot_RMSE: 5.343946, Rot_MAE: 2.764806, Rot_R2: 0.830049, Trans_MSE: 0.001065, Trans_RMSE: 0.032632, Trans_MAE: 0.021308, Trans_R2: 0.987071 +A->B:: Stage: best_test, Epoch: 70, Loss: 0.034207, Feature_alignment_loss: 0.002514, Cycle_consistency_loss: 0.002155, Scale_consensus_loss: 0.000000, Rot_MSE: 28.522974, Rot_RMSE: 5.340691, Rot_MAE: 2.761719, Rot_R2: 0.830283, Trans_MSE: 0.001124, Trans_RMSE: 0.033520, Trans_MAE: 0.022000, Trans_R2: 0.986358 +A->B:: Stage: train, Epoch: 74, Loss: 0.016120, Feature_alignment_loss: 0.002501, Cycle_consistency_loss: 0.002642, Scale_consensus_loss: 0.000000, Rot_MSE: 15.447515, Rot_RMSE: 3.930333, Rot_MAE: 2.353501, Rot_R2: 0.908723, Trans_MSE: 0.001008, Trans_RMSE: 0.031745, Trans_MAE: 0.021003, Trans_R2: 0.987860 +A->B:: Stage: test, Epoch: 74, Loss: 0.035087, Feature_alignment_loss: 0.002470, Cycle_consistency_loss: 0.002155, Scale_consensus_loss: 0.000000, Rot_MSE: 28.830299, Rot_RMSE: 5.369385, Rot_MAE: 2.789587, Rot_R2: 0.828443, Trans_MSE: 0.001157, Trans_RMSE: 0.034011, Trans_MAE: 0.022430, Trans_R2: 0.985955 +A->B:: Stage: best_test, Epoch: 70, Loss: 0.034207, Feature_alignment_loss: 0.002514, Cycle_consistency_loss: 0.002155, Scale_consensus_loss: 0.000000, Rot_MSE: 28.522974, Rot_RMSE: 5.340691, Rot_MAE: 2.761719, Rot_R2: 0.830283, Trans_MSE: 0.001124, Trans_RMSE: 0.033520, Trans_MAE: 0.022000, Trans_R2: 0.986358 +A->B:: Stage: train, Epoch: 75, Loss: 0.016337, Feature_alignment_loss: 0.002492, Cycle_consistency_loss: 0.002662, Scale_consensus_loss: 0.000000, Rot_MSE: 15.777841, Rot_RMSE: 3.972133, Rot_MAE: 2.358731, Rot_R2: 0.906794, Trans_MSE: 0.001074, Trans_RMSE: 0.032769, Trans_MAE: 0.021601, Trans_R2: 0.987065 +A->B:: Stage: test, Epoch: 75, Loss: 0.035385, Feature_alignment_loss: 0.002497, Cycle_consistency_loss: 0.002130, Scale_consensus_loss: 0.000000, Rot_MSE: 28.229391, Rot_RMSE: 5.313134, Rot_MAE: 2.760086, Rot_R2: 0.832001, Trans_MSE: 0.001191, Trans_RMSE: 0.034518, Trans_MAE: 0.022838, Trans_R2: 0.985535 +A->B:: Stage: best_test, Epoch: 70, Loss: 0.034207, Feature_alignment_loss: 0.002514, Cycle_consistency_loss: 0.002155, Scale_consensus_loss: 0.000000, Rot_MSE: 28.522974, Rot_RMSE: 5.340691, Rot_MAE: 2.761719, Rot_R2: 0.830283, Trans_MSE: 0.001124, Trans_RMSE: 0.033520, Trans_MAE: 0.022000, Trans_R2: 0.986358 +A->B:: Stage: train, Epoch: 76, Loss: 0.016093, Feature_alignment_loss: 0.002494, Cycle_consistency_loss: 0.002651, Scale_consensus_loss: 0.000000, Rot_MSE: 15.348192, Rot_RMSE: 3.917677, Rot_MAE: 2.347826, Rot_R2: 0.909327, Trans_MSE: 0.001028, Trans_RMSE: 0.032067, Trans_MAE: 0.021170, Trans_R2: 0.987612 +A->B:: Stage: test, Epoch: 76, Loss: 0.036105, Feature_alignment_loss: 0.002496, Cycle_consistency_loss: 0.002157, Scale_consensus_loss: 0.000000, Rot_MSE: 29.621889, Rot_RMSE: 5.442599, Rot_MAE: 2.786624, Rot_R2: 0.823732, Trans_MSE: 0.001180, Trans_RMSE: 0.034353, Trans_MAE: 0.022812, Trans_R2: 0.985664 +A->B:: Stage: best_test, Epoch: 70, Loss: 0.034207, Feature_alignment_loss: 0.002514, Cycle_consistency_loss: 0.002155, Scale_consensus_loss: 0.000000, Rot_MSE: 28.522974, Rot_RMSE: 5.340691, Rot_MAE: 2.761719, Rot_R2: 0.830283, Trans_MSE: 0.001124, Trans_RMSE: 0.033520, Trans_MAE: 0.022000, Trans_R2: 0.986358 +A->B:: Stage: train, Epoch: 77, Loss: 0.016223, Feature_alignment_loss: 0.002500, Cycle_consistency_loss: 0.002668, Scale_consensus_loss: 0.000000, Rot_MSE: 15.485258, Rot_RMSE: 3.935131, Rot_MAE: 2.356224, Rot_R2: 0.908520, Trans_MSE: 0.001044, Trans_RMSE: 0.032316, Trans_MAE: 0.021367, Trans_R2: 0.987420 +A->B:: Stage: test, Epoch: 77, Loss: 0.034788, Feature_alignment_loss: 0.002502, Cycle_consistency_loss: 0.002150, Scale_consensus_loss: 0.000000, Rot_MSE: 28.192852, Rot_RMSE: 5.309694, Rot_MAE: 2.738127, Rot_R2: 0.832193, Trans_MSE: 0.001137, Trans_RMSE: 0.033727, Trans_MAE: 0.021955, Trans_R2: 0.986180 +A->B:: Stage: best_test, Epoch: 70, Loss: 0.034207, Feature_alignment_loss: 0.002514, Cycle_consistency_loss: 0.002155, Scale_consensus_loss: 0.000000, Rot_MSE: 28.522974, Rot_RMSE: 5.340691, Rot_MAE: 2.761719, Rot_R2: 0.830283, Trans_MSE: 0.001124, Trans_RMSE: 0.033520, Trans_MAE: 0.022000, Trans_R2: 0.986358 +A->B:: Stage: train, Epoch: 78, Loss: 0.015882, Feature_alignment_loss: 0.002497, Cycle_consistency_loss: 0.002645, Scale_consensus_loss: 0.000000, Rot_MSE: 14.996717, Rot_RMSE: 3.872560, Rot_MAE: 2.324248, Rot_R2: 0.911371, Trans_MSE: 0.000990, Trans_RMSE: 0.031460, Trans_MAE: 0.020794, Trans_R2: 0.988078 +A->B:: Stage: test, Epoch: 78, Loss: 0.035539, Feature_alignment_loss: 0.002493, Cycle_consistency_loss: 0.002150, Scale_consensus_loss: 0.000000, Rot_MSE: 29.107563, Rot_RMSE: 5.395143, Rot_MAE: 2.788219, Rot_R2: 0.826762, Trans_MSE: 0.001168, Trans_RMSE: 0.034171, Trans_MAE: 0.022521, Trans_R2: 0.985814 +A->B:: Stage: best_test, Epoch: 70, Loss: 0.034207, Feature_alignment_loss: 0.002514, Cycle_consistency_loss: 0.002155, Scale_consensus_loss: 0.000000, Rot_MSE: 28.522974, Rot_RMSE: 5.340691, Rot_MAE: 2.761719, Rot_R2: 0.830283, Trans_MSE: 0.001124, Trans_RMSE: 0.033520, Trans_MAE: 0.022000, Trans_R2: 0.986358 +A->B:: Stage: train, Epoch: 79, Loss: 0.016228, Feature_alignment_loss: 0.002485, Cycle_consistency_loss: 0.002655, Scale_consensus_loss: 0.000000, Rot_MSE: 15.527644, Rot_RMSE: 3.940513, Rot_MAE: 2.362933, Rot_R2: 0.908253, Trans_MSE: 0.001049, Trans_RMSE: 0.032383, Trans_MAE: 0.021311, Trans_R2: 0.987366 +A->B:: Stage: test, Epoch: 79, Loss: 0.034449, Feature_alignment_loss: 0.002509, Cycle_consistency_loss: 0.002177, Scale_consensus_loss: 0.000000, Rot_MSE: 28.449762, Rot_RMSE: 5.333832, Rot_MAE: 2.757737, Rot_R2: 0.830658, Trans_MSE: 0.001078, Trans_RMSE: 0.032839, Trans_MAE: 0.021561, Trans_R2: 0.986903 +A->B:: Stage: best_test, Epoch: 70, Loss: 0.034207, Feature_alignment_loss: 0.002514, Cycle_consistency_loss: 0.002155, Scale_consensus_loss: 0.000000, Rot_MSE: 28.522974, Rot_RMSE: 5.340691, Rot_MAE: 2.761719, Rot_R2: 0.830283, Trans_MSE: 0.001124, Trans_RMSE: 0.033520, Trans_MAE: 0.022000, Trans_R2: 0.986358 +A->B:: Stage: train, Epoch: 80, Loss: 0.015974, Feature_alignment_loss: 0.002493, Cycle_consistency_loss: 0.002676, Scale_consensus_loss: 0.000000, Rot_MSE: 15.135470, Rot_RMSE: 3.890433, Rot_MAE: 2.325179, Rot_R2: 0.910536, Trans_MSE: 0.001011, Trans_RMSE: 0.031799, Trans_MAE: 0.021028, Trans_R2: 0.987819 +A->B:: Stage: test, Epoch: 80, Loss: 0.034236, Feature_alignment_loss: 0.002483, Cycle_consistency_loss: 0.002148, Scale_consensus_loss: 0.000000, Rot_MSE: 28.745407, Rot_RMSE: 5.361475, Rot_MAE: 2.767893, Rot_R2: 0.828924, Trans_MSE: 0.001066, Trans_RMSE: 0.032650, Trans_MAE: 0.021378, Trans_R2: 0.987053 +A->B:: Stage: best_test, Epoch: 70, Loss: 0.034207, Feature_alignment_loss: 0.002514, Cycle_consistency_loss: 0.002155, Scale_consensus_loss: 0.000000, Rot_MSE: 28.522974, Rot_RMSE: 5.340691, Rot_MAE: 2.761719, Rot_R2: 0.830283, Trans_MSE: 0.001124, Trans_RMSE: 0.033520, Trans_MAE: 0.022000, Trans_R2: 0.986358 +A->B:: Stage: train, Epoch: 81, Loss: 0.016167, Feature_alignment_loss: 0.002490, Cycle_consistency_loss: 0.002646, Scale_consensus_loss: 0.000000, Rot_MSE: 15.880528, Rot_RMSE: 3.985038, Rot_MAE: 2.355275, Rot_R2: 0.906139, Trans_MSE: 0.000997, Trans_RMSE: 0.031574, Trans_MAE: 0.020887, Trans_R2: 0.987991 +A->B:: Stage: test, Epoch: 81, Loss: 0.034937, Feature_alignment_loss: 0.002498, Cycle_consistency_loss: 0.002160, Scale_consensus_loss: 0.000000, Rot_MSE: 28.633495, Rot_RMSE: 5.351027, Rot_MAE: 2.753421, Rot_R2: 0.829585, Trans_MSE: 0.001179, Trans_RMSE: 0.034341, Trans_MAE: 0.022628, Trans_R2: 0.985681 +A->B:: Stage: best_test, Epoch: 70, Loss: 0.034207, Feature_alignment_loss: 0.002514, Cycle_consistency_loss: 0.002155, Scale_consensus_loss: 0.000000, Rot_MSE: 28.522974, Rot_RMSE: 5.340691, Rot_MAE: 2.761719, Rot_R2: 0.830283, Trans_MSE: 0.001124, Trans_RMSE: 0.033520, Trans_MAE: 0.022000, Trans_R2: 0.986358 +A->B:: Stage: train, Epoch: 82, Loss: 0.016011, Feature_alignment_loss: 0.002485, Cycle_consistency_loss: 0.002666, Scale_consensus_loss: 0.000000, Rot_MSE: 15.485767, Rot_RMSE: 3.935196, Rot_MAE: 2.335853, Rot_R2: 0.908450, Trans_MSE: 0.001041, Trans_RMSE: 0.032266, Trans_MAE: 0.021186, Trans_R2: 0.987459 +A->B:: Stage: test, Epoch: 82, Loss: 0.035609, Feature_alignment_loss: 0.002511, Cycle_consistency_loss: 0.002176, Scale_consensus_loss: 0.000000, Rot_MSE: 29.309591, Rot_RMSE: 5.413833, Rot_MAE: 2.775923, Rot_R2: 0.825568, Trans_MSE: 0.001139, Trans_RMSE: 0.033742, Trans_MAE: 0.022371, Trans_R2: 0.986175 +A->B:: Stage: best_test, Epoch: 70, Loss: 0.034207, Feature_alignment_loss: 0.002514, Cycle_consistency_loss: 0.002155, Scale_consensus_loss: 0.000000, Rot_MSE: 28.522974, Rot_RMSE: 5.340691, Rot_MAE: 2.761719, Rot_R2: 0.830283, Trans_MSE: 0.001124, Trans_RMSE: 0.033520, Trans_MAE: 0.022000, Trans_R2: 0.986358 +A->B:: Stage: train, Epoch: 83, Loss: 0.015994, Feature_alignment_loss: 0.002480, Cycle_consistency_loss: 0.002660, Scale_consensus_loss: 0.000000, Rot_MSE: 15.492264, Rot_RMSE: 3.936021, Rot_MAE: 2.339293, Rot_R2: 0.908394, Trans_MSE: 0.001042, Trans_RMSE: 0.032282, Trans_MAE: 0.021268, Trans_R2: 0.987446 +A->B:: Stage: test, Epoch: 83, Loss: 0.035768, Feature_alignment_loss: 0.002477, Cycle_consistency_loss: 0.002165, Scale_consensus_loss: 0.000000, Rot_MSE: 29.715956, Rot_RMSE: 5.451234, Rot_MAE: 2.780609, Rot_R2: 0.823131, Trans_MSE: 0.001105, Trans_RMSE: 0.033242, Trans_MAE: 0.021943, Trans_R2: 0.986581 +A->B:: Stage: best_test, Epoch: 70, Loss: 0.034207, Feature_alignment_loss: 0.002514, Cycle_consistency_loss: 0.002155, Scale_consensus_loss: 0.000000, Rot_MSE: 28.522974, Rot_RMSE: 5.340691, Rot_MAE: 2.761719, Rot_R2: 0.830283, Trans_MSE: 0.001124, Trans_RMSE: 0.033520, Trans_MAE: 0.022000, Trans_R2: 0.986358 +A->B:: Stage: train, Epoch: 84, Loss: 0.016232, Feature_alignment_loss: 0.002494, Cycle_consistency_loss: 0.002658, Scale_consensus_loss: 0.000000, Rot_MSE: 15.689909, Rot_RMSE: 3.961049, Rot_MAE: 2.349464, Rot_R2: 0.907309, Trans_MSE: 0.001037, Trans_RMSE: 0.032201, Trans_MAE: 0.021274, Trans_R2: 0.987508 +A->B:: Stage: test, Epoch: 84, Loss: 0.036460, Feature_alignment_loss: 0.002491, Cycle_consistency_loss: 0.002172, Scale_consensus_loss: 0.000000, Rot_MSE: 30.280115, Rot_RMSE: 5.502737, Rot_MAE: 2.806371, Rot_R2: 0.819778, Trans_MSE: 0.001111, Trans_RMSE: 0.033333, Trans_MAE: 0.022075, Trans_R2: 0.986508 +A->B:: Stage: best_test, Epoch: 70, Loss: 0.034207, Feature_alignment_loss: 0.002514, Cycle_consistency_loss: 0.002155, Scale_consensus_loss: 0.000000, Rot_MSE: 28.522974, Rot_RMSE: 5.340691, Rot_MAE: 2.761719, Rot_R2: 0.830283, Trans_MSE: 0.001124, Trans_RMSE: 0.033520, Trans_MAE: 0.022000, Trans_R2: 0.986358 +A->B:: Stage: train, Epoch: 85, Loss: 0.016095, Feature_alignment_loss: 0.002494, Cycle_consistency_loss: 0.002654, Scale_consensus_loss: 0.000000, Rot_MSE: 15.486405, Rot_RMSE: 3.935277, Rot_MAE: 2.352478, Rot_R2: 0.908512, Trans_MSE: 0.000987, Trans_RMSE: 0.031419, Trans_MAE: 0.020963, Trans_R2: 0.988109 +A->B:: Stage: test, Epoch: 85, Loss: 0.035404, Feature_alignment_loss: 0.002494, Cycle_consistency_loss: 0.002149, Scale_consensus_loss: 0.000000, Rot_MSE: 29.241224, Rot_RMSE: 5.407516, Rot_MAE: 2.761086, Rot_R2: 0.825983, Trans_MSE: 0.001171, Trans_RMSE: 0.034215, Trans_MAE: 0.022627, Trans_R2: 0.985791 +A->B:: Stage: best_test, Epoch: 70, Loss: 0.034207, Feature_alignment_loss: 0.002514, Cycle_consistency_loss: 0.002155, Scale_consensus_loss: 0.000000, Rot_MSE: 28.522974, Rot_RMSE: 5.340691, Rot_MAE: 2.761719, Rot_R2: 0.830283, Trans_MSE: 0.001124, Trans_RMSE: 0.033520, Trans_MAE: 0.022000, Trans_R2: 0.986358 +A->B:: Stage: train, Epoch: 86, Loss: 0.015922, Feature_alignment_loss: 0.002492, Cycle_consistency_loss: 0.002660, Scale_consensus_loss: 0.000000, Rot_MSE: 14.898924, Rot_RMSE: 3.859912, Rot_MAE: 2.330456, Rot_R2: 0.911987, Trans_MSE: 0.001054, Trans_RMSE: 0.032463, Trans_MAE: 0.021269, Trans_R2: 0.987306 +A->B:: Stage: test, Epoch: 86, Loss: 0.035199, Feature_alignment_loss: 0.002481, Cycle_consistency_loss: 0.002162, Scale_consensus_loss: 0.000000, Rot_MSE: 29.012167, Rot_RMSE: 5.386294, Rot_MAE: 2.761548, Rot_R2: 0.827317, Trans_MSE: 0.001170, Trans_RMSE: 0.034210, Trans_MAE: 0.022627, Trans_R2: 0.985791 +A->B:: Stage: best_test, Epoch: 70, Loss: 0.034207, Feature_alignment_loss: 0.002514, Cycle_consistency_loss: 0.002155, Scale_consensus_loss: 0.000000, Rot_MSE: 28.522974, Rot_RMSE: 5.340691, Rot_MAE: 2.761719, Rot_R2: 0.830283, Trans_MSE: 0.001124, Trans_RMSE: 0.033520, Trans_MAE: 0.022000, Trans_R2: 0.986358 +A->B:: Stage: train, Epoch: 87, Loss: 0.016000, Feature_alignment_loss: 0.002499, Cycle_consistency_loss: 0.002630, Scale_consensus_loss: 0.000000, Rot_MSE: 14.951271, Rot_RMSE: 3.866687, Rot_MAE: 2.322006, Rot_R2: 0.911658, Trans_MSE: 0.001031, Trans_RMSE: 0.032110, Trans_MAE: 0.021135, Trans_R2: 0.987580 +A->B:: Stage: test, Epoch: 87, Loss: 0.035010, Feature_alignment_loss: 0.002453, Cycle_consistency_loss: 0.002145, Scale_consensus_loss: 0.000000, Rot_MSE: 29.381222, Rot_RMSE: 5.420445, Rot_MAE: 2.770063, Rot_R2: 0.825123, Trans_MSE: 0.001111, Trans_RMSE: 0.033330, Trans_MAE: 0.021871, Trans_R2: 0.986511 +A->B:: Stage: best_test, Epoch: 70, Loss: 0.034207, Feature_alignment_loss: 0.002514, Cycle_consistency_loss: 0.002155, Scale_consensus_loss: 0.000000, Rot_MSE: 28.522974, Rot_RMSE: 5.340691, Rot_MAE: 2.761719, Rot_R2: 0.830283, Trans_MSE: 0.001124, Trans_RMSE: 0.033520, Trans_MAE: 0.022000, Trans_R2: 0.986358 +A->B:: Stage: train, Epoch: 88, Loss: 0.016189, Feature_alignment_loss: 0.002488, Cycle_consistency_loss: 0.002650, Scale_consensus_loss: 0.000000, Rot_MSE: 15.887966, Rot_RMSE: 3.985971, Rot_MAE: 2.349849, Rot_R2: 0.906125, Trans_MSE: 0.001033, Trans_RMSE: 0.032139, Trans_MAE: 0.021128, Trans_R2: 0.987558 +A->B:: Stage: test, Epoch: 88, Loss: 0.035987, Feature_alignment_loss: 0.002508, Cycle_consistency_loss: 0.002189, Scale_consensus_loss: 0.000000, Rot_MSE: 29.261810, Rot_RMSE: 5.409419, Rot_MAE: 2.784417, Rot_R2: 0.825837, Trans_MSE: 0.001181, Trans_RMSE: 0.034359, Trans_MAE: 0.022727, Trans_R2: 0.985664 +A->B:: Stage: best_test, Epoch: 70, Loss: 0.034207, Feature_alignment_loss: 0.002514, Cycle_consistency_loss: 0.002155, Scale_consensus_loss: 0.000000, Rot_MSE: 28.522974, Rot_RMSE: 5.340691, Rot_MAE: 2.761719, Rot_R2: 0.830283, Trans_MSE: 0.001124, Trans_RMSE: 0.033520, Trans_MAE: 0.022000, Trans_R2: 0.986358 +A->B:: Stage: train, Epoch: 89, Loss: 0.016310, Feature_alignment_loss: 0.002491, Cycle_consistency_loss: 0.002664, Scale_consensus_loss: 0.000000, Rot_MSE: 16.181925, Rot_RMSE: 4.022676, Rot_MAE: 2.342423, Rot_R2: 0.904433, Trans_MSE: 0.000997, Trans_RMSE: 0.031577, Trans_MAE: 0.020887, Trans_R2: 0.987988 +A->B:: Stage: test, Epoch: 89, Loss: 0.035752, Feature_alignment_loss: 0.002502, Cycle_consistency_loss: 0.002154, Scale_consensus_loss: 0.000000, Rot_MSE: 28.900856, Rot_RMSE: 5.375952, Rot_MAE: 2.765131, Rot_R2: 0.828000, Trans_MSE: 0.001166, Trans_RMSE: 0.034147, Trans_MAE: 0.022672, Trans_R2: 0.985842 +A->B:: Stage: best_test, Epoch: 70, Loss: 0.034207, Feature_alignment_loss: 0.002514, Cycle_consistency_loss: 0.002155, Scale_consensus_loss: 0.000000, Rot_MSE: 28.522974, Rot_RMSE: 5.340691, Rot_MAE: 2.761719, Rot_R2: 0.830283, Trans_MSE: 0.001124, Trans_RMSE: 0.033520, Trans_MAE: 0.022000, Trans_R2: 0.986358 +A->B:: Stage: train, Epoch: 90, Loss: 0.016598, Feature_alignment_loss: 0.002481, Cycle_consistency_loss: 0.002657, Scale_consensus_loss: 0.000000, Rot_MSE: 18.900831, Rot_RMSE: 4.347508, Rot_MAE: 2.361913, Rot_R2: 0.888122, Trans_MSE: 0.001029, Trans_RMSE: 0.032079, Trans_MAE: 0.021128, Trans_R2: 0.987603 +A->B:: Stage: test, Epoch: 90, Loss: 0.034780, Feature_alignment_loss: 0.002504, Cycle_consistency_loss: 0.002170, Scale_consensus_loss: 0.000000, Rot_MSE: 28.646423, Rot_RMSE: 5.352235, Rot_MAE: 2.757438, Rot_R2: 0.829520, Trans_MSE: 0.001156, Trans_RMSE: 0.034003, Trans_MAE: 0.022221, Trans_R2: 0.985961 +A->B:: Stage: best_test, Epoch: 70, Loss: 0.034207, Feature_alignment_loss: 0.002514, Cycle_consistency_loss: 0.002155, Scale_consensus_loss: 0.000000, Rot_MSE: 28.522974, Rot_RMSE: 5.340691, Rot_MAE: 2.761719, Rot_R2: 0.830283, Trans_MSE: 0.001124, Trans_RMSE: 0.033520, Trans_MAE: 0.022000, Trans_R2: 0.986358 +A->B:: Stage: train, Epoch: 91, Loss: 0.015833, Feature_alignment_loss: 0.002491, Cycle_consistency_loss: 0.002666, Scale_consensus_loss: 0.000000, Rot_MSE: 14.870306, Rot_RMSE: 3.856204, Rot_MAE: 2.319400, Rot_R2: 0.912116, Trans_MSE: 0.001038, Trans_RMSE: 0.032217, Trans_MAE: 0.021297, Trans_R2: 0.987495 +A->B:: Stage: test, Epoch: 91, Loss: 0.035942, Feature_alignment_loss: 0.002511, Cycle_consistency_loss: 0.002194, Scale_consensus_loss: 0.000000, Rot_MSE: 29.382378, Rot_RMSE: 5.420551, Rot_MAE: 2.795511, Rot_R2: 0.825150, Trans_MSE: 0.001222, Trans_RMSE: 0.034954, Trans_MAE: 0.023184, Trans_R2: 0.985161 +A->B:: Stage: best_test, Epoch: 70, Loss: 0.034207, Feature_alignment_loss: 0.002514, Cycle_consistency_loss: 0.002155, Scale_consensus_loss: 0.000000, Rot_MSE: 28.522974, Rot_RMSE: 5.340691, Rot_MAE: 2.761719, Rot_R2: 0.830283, Trans_MSE: 0.001124, Trans_RMSE: 0.033520, Trans_MAE: 0.022000, Trans_R2: 0.986358 +A->B:: Stage: train, Epoch: 92, Loss: 0.016035, Feature_alignment_loss: 0.002489, Cycle_consistency_loss: 0.002673, Scale_consensus_loss: 0.000000, Rot_MSE: 14.978808, Rot_RMSE: 3.870247, Rot_MAE: 2.334298, Rot_R2: 0.911509, Trans_MSE: 0.001053, Trans_RMSE: 0.032450, Trans_MAE: 0.021378, Trans_R2: 0.987317 +A->B:: Stage: test, Epoch: 92, Loss: 0.034898, Feature_alignment_loss: 0.002490, Cycle_consistency_loss: 0.002180, Scale_consensus_loss: 0.000000, Rot_MSE: 28.991119, Rot_RMSE: 5.384340, Rot_MAE: 2.771603, Rot_R2: 0.827476, Trans_MSE: 0.001147, Trans_RMSE: 0.033865, Trans_MAE: 0.022305, Trans_R2: 0.986073 +A->B:: Stage: best_test, Epoch: 70, Loss: 0.034207, Feature_alignment_loss: 0.002514, Cycle_consistency_loss: 0.002155, Scale_consensus_loss: 0.000000, Rot_MSE: 28.522974, Rot_RMSE: 5.340691, Rot_MAE: 2.761719, Rot_R2: 0.830283, Trans_MSE: 0.001124, Trans_RMSE: 0.033520, Trans_MAE: 0.022000, Trans_R2: 0.986358 +A->B:: Stage: train, Epoch: 93, Loss: 0.016143, Feature_alignment_loss: 0.002492, Cycle_consistency_loss: 0.002653, Scale_consensus_loss: 0.000000, Rot_MSE: 15.485974, Rot_RMSE: 3.935222, Rot_MAE: 2.334501, Rot_R2: 0.908479, Trans_MSE: 0.001055, Trans_RMSE: 0.032475, Trans_MAE: 0.021191, Trans_R2: 0.987295 +A->B:: Stage: test, Epoch: 93, Loss: 0.034664, Feature_alignment_loss: 0.002487, Cycle_consistency_loss: 0.002152, Scale_consensus_loss: 0.000000, Rot_MSE: 28.698643, Rot_RMSE: 5.357111, Rot_MAE: 2.746253, Rot_R2: 0.829182, Trans_MSE: 0.001125, Trans_RMSE: 0.033547, Trans_MAE: 0.021961, Trans_R2: 0.986336 +A->B:: Stage: best_test, Epoch: 70, Loss: 0.034207, Feature_alignment_loss: 0.002514, Cycle_consistency_loss: 0.002155, Scale_consensus_loss: 0.000000, Rot_MSE: 28.522974, Rot_RMSE: 5.340691, Rot_MAE: 2.761719, Rot_R2: 0.830283, Trans_MSE: 0.001124, Trans_RMSE: 0.033520, Trans_MAE: 0.022000, Trans_R2: 0.986358 +A->B:: Stage: train, Epoch: 94, Loss: 0.016398, Feature_alignment_loss: 0.002488, Cycle_consistency_loss: 0.002654, Scale_consensus_loss: 0.000000, Rot_MSE: 16.243856, Rot_RMSE: 4.030367, Rot_MAE: 2.369636, Rot_R2: 0.904033, Trans_MSE: 0.001030, Trans_RMSE: 0.032098, Trans_MAE: 0.021291, Trans_R2: 0.987589 +A->B:: Stage: test, Epoch: 94, Loss: 0.035401, Feature_alignment_loss: 0.002494, Cycle_consistency_loss: 0.002180, Scale_consensus_loss: 0.000000, Rot_MSE: 29.117235, Rot_RMSE: 5.396039, Rot_MAE: 2.769846, Rot_R2: 0.826742, Trans_MSE: 0.001170, Trans_RMSE: 0.034209, Trans_MAE: 0.022688, Trans_R2: 0.985792 +A->B:: Stage: best_test, Epoch: 70, Loss: 0.034207, Feature_alignment_loss: 0.002514, Cycle_consistency_loss: 0.002155, Scale_consensus_loss: 0.000000, Rot_MSE: 28.522974, Rot_RMSE: 5.340691, Rot_MAE: 2.761719, Rot_R2: 0.830283, Trans_MSE: 0.001124, Trans_RMSE: 0.033520, Trans_MAE: 0.022000, Trans_R2: 0.986358 +A->B:: Stage: train, Epoch: 95, Loss: 0.015953, Feature_alignment_loss: 0.002489, Cycle_consistency_loss: 0.002668, Scale_consensus_loss: 0.000000, Rot_MSE: 15.543718, Rot_RMSE: 3.942552, Rot_MAE: 2.351323, Rot_R2: 0.908106, Trans_MSE: 0.001003, Trans_RMSE: 0.031668, Trans_MAE: 0.021004, Trans_R2: 0.987919 +A->B:: Stage: test, Epoch: 95, Loss: 0.035291, Feature_alignment_loss: 0.002519, Cycle_consistency_loss: 0.002184, Scale_consensus_loss: 0.000000, Rot_MSE: 29.005861, Rot_RMSE: 5.385709, Rot_MAE: 2.773478, Rot_R2: 0.827398, Trans_MSE: 0.001139, Trans_RMSE: 0.033752, Trans_MAE: 0.022302, Trans_R2: 0.986165 +A->B:: Stage: best_test, Epoch: 70, Loss: 0.034207, Feature_alignment_loss: 0.002514, Cycle_consistency_loss: 0.002155, Scale_consensus_loss: 0.000000, Rot_MSE: 28.522974, Rot_RMSE: 5.340691, Rot_MAE: 2.761719, Rot_R2: 0.830283, Trans_MSE: 0.001124, Trans_RMSE: 0.033520, Trans_MAE: 0.022000, Trans_R2: 0.986358 +A->B:: Stage: train, Epoch: 96, Loss: 0.015963, Feature_alignment_loss: 0.002486, Cycle_consistency_loss: 0.002654, Scale_consensus_loss: 0.000000, Rot_MSE: 16.637527, Rot_RMSE: 4.078913, Rot_MAE: 2.324991, Rot_R2: 0.901549, Trans_MSE: 0.001017, Trans_RMSE: 0.031897, Trans_MAE: 0.021110, Trans_R2: 0.987744 +A->B:: Stage: test, Epoch: 96, Loss: 0.036689, Feature_alignment_loss: 0.002513, Cycle_consistency_loss: 0.002168, Scale_consensus_loss: 0.000000, Rot_MSE: 29.240986, Rot_RMSE: 5.407494, Rot_MAE: 2.778793, Rot_R2: 0.826035, Trans_MSE: 0.001248, Trans_RMSE: 0.035325, Trans_MAE: 0.023544, Trans_R2: 0.984843 +A->B:: Stage: best_test, Epoch: 70, Loss: 0.034207, Feature_alignment_loss: 0.002514, Cycle_consistency_loss: 0.002155, Scale_consensus_loss: 0.000000, Rot_MSE: 28.522974, Rot_RMSE: 5.340691, Rot_MAE: 2.761719, Rot_R2: 0.830283, Trans_MSE: 0.001124, Trans_RMSE: 0.033520, Trans_MAE: 0.022000, Trans_R2: 0.986358 +A->B:: Stage: train, Epoch: 97, Loss: 0.016030, Feature_alignment_loss: 0.002482, Cycle_consistency_loss: 0.002629, Scale_consensus_loss: 0.000000, Rot_MSE: 15.206112, Rot_RMSE: 3.899502, Rot_MAE: 2.324821, Rot_R2: 0.910165, Trans_MSE: 0.001043, Trans_RMSE: 0.032299, Trans_MAE: 0.021244, Trans_R2: 0.987433 +A->B:: Stage: test, Epoch: 97, Loss: 0.035443, Feature_alignment_loss: 0.002488, Cycle_consistency_loss: 0.002179, Scale_consensus_loss: 0.000000, Rot_MSE: 28.813795, Rot_RMSE: 5.367848, Rot_MAE: 2.757340, Rot_R2: 0.828525, Trans_MSE: 0.001238, Trans_RMSE: 0.035190, Trans_MAE: 0.023433, Trans_R2: 0.984949 +A->B:: Stage: best_test, Epoch: 70, Loss: 0.034207, Feature_alignment_loss: 0.002514, Cycle_consistency_loss: 0.002155, Scale_consensus_loss: 0.000000, Rot_MSE: 28.522974, Rot_RMSE: 5.340691, Rot_MAE: 2.761719, Rot_R2: 0.830283, Trans_MSE: 0.001124, Trans_RMSE: 0.033520, Trans_MAE: 0.022000, Trans_R2: 0.986358 +A->B:: Stage: train, Epoch: 98, Loss: 0.015819, Feature_alignment_loss: 0.002487, Cycle_consistency_loss: 0.002633, Scale_consensus_loss: 0.000000, Rot_MSE: 15.068851, Rot_RMSE: 3.881862, Rot_MAE: 2.315337, Rot_R2: 0.910940, Trans_MSE: 0.000999, Trans_RMSE: 0.031600, Trans_MAE: 0.021029, Trans_R2: 0.987970 +A->B:: Stage: test, Epoch: 98, Loss: 0.035630, Feature_alignment_loss: 0.002485, Cycle_consistency_loss: 0.002172, Scale_consensus_loss: 0.000000, Rot_MSE: 29.539967, Rot_RMSE: 5.435068, Rot_MAE: 2.787704, Rot_R2: 0.824195, Trans_MSE: 0.001131, Trans_RMSE: 0.033630, Trans_MAE: 0.022133, Trans_R2: 0.986269 +A->B:: Stage: best_test, Epoch: 70, Loss: 0.034207, Feature_alignment_loss: 0.002514, Cycle_consistency_loss: 0.002155, Scale_consensus_loss: 0.000000, Rot_MSE: 28.522974, Rot_RMSE: 5.340691, Rot_MAE: 2.761719, Rot_R2: 0.830283, Trans_MSE: 0.001124, Trans_RMSE: 0.033520, Trans_MAE: 0.022000, Trans_R2: 0.986358 +A->B:: Stage: train, Epoch: 99, Loss: 0.015971, Feature_alignment_loss: 0.002479, Cycle_consistency_loss: 0.002656, Scale_consensus_loss: 0.000000, Rot_MSE: 15.291389, Rot_RMSE: 3.910421, Rot_MAE: 2.312800, Rot_R2: 0.909688, Trans_MSE: 0.001032, Trans_RMSE: 0.032130, Trans_MAE: 0.021245, Trans_R2: 0.987564 +A->B:: Stage: test, Epoch: 99, Loss: 0.035399, Feature_alignment_loss: 0.002507, Cycle_consistency_loss: 0.002171, Scale_consensus_loss: 0.000000, Rot_MSE: 29.296185, Rot_RMSE: 5.412595, Rot_MAE: 2.789558, Rot_R2: 0.825686, Trans_MSE: 0.001155, Trans_RMSE: 0.033982, Trans_MAE: 0.022388, Trans_R2: 0.985988 +A->B:: Stage: best_test, Epoch: 70, Loss: 0.034207, Feature_alignment_loss: 0.002514, Cycle_consistency_loss: 0.002155, Scale_consensus_loss: 0.000000, Rot_MSE: 28.522974, Rot_RMSE: 5.340691, Rot_MAE: 2.761719, Rot_R2: 0.830283, Trans_MSE: 0.001124, Trans_RMSE: 0.033520, Trans_MAE: 0.022000, Trans_R2: 0.986358 diff --git a/thirdparty/learning3d/pretrained/exp_prnet/models/best_model.t7 b/thirdparty/learning3d/pretrained/exp_prnet/models/best_model.t7 new file mode 100644 index 0000000000000000000000000000000000000000..1b93af5e779b767f948e9c601c99119aeda8f6e3 --- /dev/null +++ b/thirdparty/learning3d/pretrained/exp_prnet/models/best_model.t7 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7a927d3a9d7afd73d0c8f8c3489205781094dcd9d248fc26fecc529dc9038353 +size 22887712 diff --git a/thirdparty/learning3d/pretrained/exp_prnet/models/model.99.t7 b/thirdparty/learning3d/pretrained/exp_prnet/models/model.99.t7 new file mode 100644 index 0000000000000000000000000000000000000000..31c69be082aaaa98be6b1787d51904037b56c706 --- /dev/null +++ b/thirdparty/learning3d/pretrained/exp_prnet/models/model.99.t7 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3333de53c61b663e5ef842e85996a037582b76e52bf6eb13301bc1ce2c410f20 +size 22887712 diff --git a/thirdparty/learning3d/pretrained/exp_rpmnet/models/clean-trained.pth b/thirdparty/learning3d/pretrained/exp_rpmnet/models/clean-trained.pth new file mode 100644 index 0000000000000000000000000000000000000000..82dc29edd6e31a3bf1dfb7afde16b455a11b2d0f --- /dev/null +++ b/thirdparty/learning3d/pretrained/exp_rpmnet/models/clean-trained.pth @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a60b1720929b52c6b8746b28b8878b6776fbc041e1f258966fb21b2f2b601902 +size 10886460 diff --git a/thirdparty/learning3d/pretrained/exp_rpmnet/models/noisy-trained.pth b/thirdparty/learning3d/pretrained/exp_rpmnet/models/noisy-trained.pth new file mode 100644 index 0000000000000000000000000000000000000000..1c410687cd3fe6c90846a166dbb4781910ec655c --- /dev/null +++ b/thirdparty/learning3d/pretrained/exp_rpmnet/models/noisy-trained.pth @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9f2790487a347c96146e293b8c46205353c5220287b5d602289393b1fe0e4249 +size 10886184 diff --git a/thirdparty/learning3d/pretrained/exp_rpmnet/models/partial-trained.pth b/thirdparty/learning3d/pretrained/exp_rpmnet/models/partial-trained.pth new file mode 100644 index 0000000000000000000000000000000000000000..c96e41006998474d42ea3c1abb7bc26f43171d15 --- /dev/null +++ b/thirdparty/learning3d/pretrained/exp_rpmnet/models/partial-trained.pth @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4086745fb077cc4679638d3d7a0e63f1c01a395f8e890594be14c65cecc52c79 +size 10886584 diff --git a/thirdparty/learning3d/utils/__init__.py b/thirdparty/learning3d/utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..13da43bf560957fa18f3850d88c0e3a0338c5011 --- /dev/null +++ b/thirdparty/learning3d/utils/__init__.py @@ -0,0 +1,23 @@ +from .svd import SVDHead +from .transformer import Transformer, Identity +from .ppfnet_util import angle_difference, square_distance, index_points, farthest_point_sample, query_ball_point, sample_and_group, sample_and_group_multi +from .pointconv_util import PointConvDensitySetAbstraction +from .model_common_utils import ( + knn, + pc_normalize, + square_distance, + index_points, + farthest_point_sample, + knn_point, + query_ball_point, + get_graph_feature +) +from .curvenet_util import ( + LPFA, + CIC, +) + +try: + from .lib import pointnet2_utils +except: + print("Error raised in pointnet2 module in utils!\nEither don't use pointnet2_utils or retry it's setup.") \ No newline at end of file diff --git a/thirdparty/learning3d/utils/curvenet_util.py b/thirdparty/learning3d/utils/curvenet_util.py new file mode 100644 index 0000000000000000000000000000000000000000..56e02c52357abfbe572afff850ee1ddf31dbe331 --- /dev/null +++ b/thirdparty/learning3d/utils/curvenet_util.py @@ -0,0 +1,540 @@ +""" +@Author: Yue Wang +@Contact: yuewangx@mit.edu +@File: pointnet_util.py +@Time: 2018/10/13 10:39 PM + +Modified by +@Author: Tiange Xiang +@Contact: txia7609@uni.sydney.edu.au +@Time: 2021/01/21 3:10 PM +""" + +import torch +import torch.nn as nn +import torch.nn.functional as F +from time import time +import numpy as np +from .model_common_utils import ( + knn, + square_distance, + index_points, + farthest_point_sample, + query_ball_point, +) + +def sample_and_group(npoint, radius, nsample, xyz, points, returnfps=False): + """ + Input: + npoint: + radius: + nsample: + xyz: input points position data, [B, N, 3] + points: input points data, [B, N, D] + Return: + new_xyz: sampled points position data, [B, npoint, nsample, 3] + new_points: sampled points data, [B, npoint, nsample, 3+D] + """ + new_xyz = index_points(xyz, farthest_point_sample(xyz, npoint, start_with_first_point=True)) + torch.cuda.empty_cache() + + idx = query_ball_point(radius, nsample, xyz, new_xyz, get_cnt=False) + torch.cuda.empty_cache() + + new_points = index_points(points, idx) + torch.cuda.empty_cache() + + if returnfps: + return new_xyz, new_points, idx + else: + return new_xyz, new_points + +def batched_index_select(input, dim, index): + views = [input.shape[0]] + \ + [1 if i != dim else -1 for i in range(1, len(input.shape))] + expanse = list(input.shape) + expanse[0] = -1 + expanse[dim] = -1 + index = index.view(views).expand(expanse) + return torch.gather(input, dim, index) + +def gumbel_softmax(logits, dim, temperature=1): + """ + ST-gumple-softmax w/o random gumbel samplings + input: [*, n_class] + return: flatten --> [*, n_class] an one-hot vector + """ + y = F.softmax(logits / temperature, dim=dim) + + shape = y.size() + _, ind = y.max(dim=-1) + y_hard = torch.zeros_like(y).view(-1, shape[-1]) + y_hard.scatter_(1, ind.view(-1, 1), 1) + y_hard = y_hard.view(*shape) + + y_hard = (y_hard - y).detach() + y + return y_hard + +class Walk(nn.Module): + ''' + Walk in the cloud + ''' + def __init__(self, in_channel, k, curve_num, curve_length): + super(Walk, self).__init__() + self.curve_num = curve_num + self.curve_length = curve_length + self.k = k + + self.agent_mlp = nn.Sequential( + nn.Conv2d(in_channel * 2, + 1, + kernel_size=1, + bias=False), nn.BatchNorm2d(1)) + self.momentum_mlp = nn.Sequential( + nn.Conv1d(in_channel * 2, + 2, + kernel_size=1, + bias=False), nn.BatchNorm1d(2)) + + def crossover_suppression(self, cur, neighbor, bn, n, k): + # cur: bs*n, 3 + # neighbor: bs*n, 3, k + neighbor = neighbor.detach() + cur = cur.unsqueeze(-1).detach() + dot = torch.bmm(cur.transpose(1,2), neighbor) # bs*n, 1, k + norm1 = torch.norm(cur, dim=1, keepdim=True) + norm2 = torch.norm(neighbor, dim=1, keepdim=True) + divider = torch.clamp(norm1 * norm2, min=1e-8) + ans = torch.div(dot, divider).squeeze() # bs*n, k + + # normalize to [0, 1] + ans = 1. + ans + ans = torch.clamp(ans, 0., 1.0) + + return ans.detach() + + def forward(self, xyz, x, adj, cur): + bn, c, tot_points = x.size() + device = x.device + + # raw point coordinates + xyz = xyz.transpose(1,2).contiguous # bs, n, 3 + + # point features + x = x.transpose(1,2).contiguous() # bs, n, c + + flatten_x = x.view(bn * tot_points, -1) + batch_offset = torch.arange(0, bn, device=device).detach() * tot_points + + # indices of neighbors for the starting points + tmp_adj = (adj + batch_offset.view(-1,1,1)).view(adj.size(0)*adj.size(1),-1) #bs, n, k + + # batch flattened indices for teh starting points + flatten_cur = (cur + batch_offset.view(-1,1,1)).view(-1) + + curves = [] + flatten_curve_idxs = [flatten_cur.unsqueeze(1)] + + # one step at a time + for step in range(self.curve_length): + + if step == 0: + # get starting point features using flattend indices + starting_points = flatten_x[flatten_cur, :].contiguous() + pre_feature = starting_points.view(bn, self.curve_num, -1, 1).transpose(1,2) # bs * n, c + else: + # dynamic momentum + cat_feature = torch.cat((cur_feature.squeeze(-1), pre_feature.squeeze(-1)),dim=1) + att_feature = F.softmax(self.momentum_mlp(cat_feature),dim=1).view(bn, 1, self.curve_num, 2) # bs, 1, n, 2 + cat_feature = torch.cat((cur_feature, pre_feature),dim=-1) # bs, c, n, 2 + + # update curve descriptor + pre_feature = torch.sum(cat_feature * att_feature, dim=-1, keepdim=True) # bs, c, n + pre_feature_cos = pre_feature.transpose(1,2).contiguous().view(bn * self.curve_num, -1) + + pick_idx = tmp_adj[flatten_cur] # bs*n, k + + # get the neighbors of current points + pick_values = flatten_x[pick_idx.view(-1),:] + + # reshape to fit crossover suppresion below + pick_values_cos = pick_values.view(bn * self.curve_num, self.k, c) + pick_values = pick_values_cos.view(bn, self.curve_num, self.k, c) + pick_values_cos = pick_values_cos.transpose(1,2).contiguous() + + pick_values = pick_values.permute(0,3,1,2) # bs, c, n, k + + pre_feature_expand = pre_feature.expand_as(pick_values) + + # concat current point features with curve descriptors + pre_feature_expand = torch.cat((pick_values, pre_feature_expand),dim=1) + + # which node to pick next? + pre_feature_expand = self.agent_mlp(pre_feature_expand) # bs, 1, n, k + + if step !=0: + # cross over supression + d = self.crossover_suppression(cur_feature_cos - pre_feature_cos, + pick_values_cos - cur_feature_cos.unsqueeze(-1), + bn, self.curve_num, self.k) + d = d.view(bn, self.curve_num, self.k).unsqueeze(1) # bs, 1, n, k + pre_feature_expand = torch.mul(pre_feature_expand, d) + + pre_feature_expand = gumbel_softmax(pre_feature_expand, -1) #bs, 1, n, k + + cur_feature = torch.sum(pick_values * pre_feature_expand, dim=-1, keepdim=True) # bs, c, n, 1 + + cur_feature_cos = cur_feature.transpose(1,2).contiguous().view(bn * self.curve_num, c) + + cur = torch.argmax(pre_feature_expand, dim=-1).view(-1, 1) # bs * n, 1 + + flatten_cur = batched_index_select(pick_idx, 1, cur).squeeze() # bs * n + + # collect curve progress + curves.append(cur_feature) + flatten_curve_idxs.append(flatten_cur.unsqueeze(1)) + + return torch.cat(curves,dim=-1), torch.cat(flatten_curve_idxs, dim=1) + + +class Attention_block(nn.Module): + ''' + Used in attention U-Net. + ''' + def __init__(self,F_g,F_l,F_int): + super(Attention_block,self).__init__() + self.W_g = nn.Sequential( + nn.Conv1d(F_g, F_int, kernel_size=1,stride=1,padding=0,bias=True), + nn.BatchNorm1d(F_int) + ) + + self.W_x = nn.Sequential( + nn.Conv1d(F_l, F_int, kernel_size=1,stride=1,padding=0,bias=True), + nn.BatchNorm1d(F_int) + ) + + self.psi = nn.Sequential( + nn.Conv1d(F_int, 1, kernel_size=1,stride=1,padding=0,bias=True), + nn.BatchNorm1d(1), + nn.Sigmoid() + ) + + def forward(self,g,x): + g1 = self.W_g(g) + x1 = self.W_x(x) + psi = F.leaky_relu(g1+x1, negative_slope=0.2) + psi = self.psi(psi) + + return psi, 1. - psi + + +class LPFA(nn.Module): + def __init__(self, in_channel, out_channel, k, mlp_num=2, initial=False): + super(LPFA, self).__init__() + self.k = k + self.initial = initial + + if not initial: + self.xyz2feature = nn.Sequential( + nn.Conv2d(9, in_channel, kernel_size=1, bias=False), + nn.BatchNorm2d(in_channel)) + + self.mlp = [] + for _ in range(mlp_num): + self.mlp.append(nn.Sequential(nn.Conv2d(in_channel, out_channel, 1, bias=False), + nn.BatchNorm2d(out_channel), + nn.LeakyReLU(0.2))) + in_channel = out_channel + self.mlp = nn.Sequential(*self.mlp) + + def forward(self, x, xyz, idx=None): + x = self.group_feature(x, xyz, idx) + x = self.mlp(x) + + if self.initial: + x = x.max(dim=-1, keepdim=False)[0] + else: + x = x.mean(dim=-1, keepdim=False) + + return x + + def group_feature(self, x, xyz, idx): + batch_size, num_dims, num_points = x.size() + device = x.device + + if idx is None: + idx = knn(xyz, k=self.k, add_one_to_k=True)[:,:,:self.k] # (batch_size, num_points, k) + + idx_base = torch.arange(0, batch_size, device=device).view(-1, 1, 1) * num_points + idx = idx + idx_base + idx = idx.view(-1) + + xyz = xyz.transpose(2, 1).contiguous() # bs, n, 3 + point_feature = xyz.view(batch_size * num_points, -1)[idx, :] + point_feature = point_feature.view(batch_size, num_points, self.k, -1) # bs, n, k, 3 + points = xyz.view(batch_size, num_points, 1, 3).expand(-1, -1, self.k, -1) # bs, n, k, 3 + + point_feature = torch.cat((points, point_feature, point_feature - points), + dim=3).permute(0, 3, 1, 2).contiguous() + + if self.initial: + return point_feature + + x = x.transpose(2, 1).contiguous() # bs, n, c + feature = x.view(batch_size * num_points, -1)[idx, :] + feature = feature.view(batch_size, num_points, self.k, num_dims) #bs, n, k, c + x = x.view(batch_size, num_points, 1, num_dims) + feature = feature - x + + feature = feature.permute(0, 3, 1, 2).contiguous() + point_feature = self.xyz2feature(point_feature) #bs, c, n, k + feature = F.leaky_relu(feature + point_feature, 0.2) + return feature #bs, c, n, k + + +class PointNetFeaturePropagation(nn.Module): + def __init__(self, in_channel, mlp, att=None): + super(PointNetFeaturePropagation, self).__init__() + self.mlp_convs = nn.ModuleList() + self.mlp_bns = nn.ModuleList() + last_channel = in_channel + self.att = None + if att is not None: + self.att = Attention_block(F_g=att[0],F_l=att[1],F_int=att[2]) + + for out_channel in mlp: + self.mlp_convs.append(nn.Conv1d(last_channel, out_channel, 1)) + self.mlp_bns.append(nn.BatchNorm1d(out_channel)) + last_channel = out_channel + + def forward(self, xyz1, xyz2, points1, points2): + """ + Input: + xyz1: input points position data, [B, C, N] + xyz2: sampled input points position data, [B, C, S], skipped xyz + points1: input points data, [B, D, N] + points2: input points data, [B, D, S], skipped features + Return: + new_points: upsampled points data, [B, D', N] + """ + xyz1 = xyz1.permute(0, 2, 1) + xyz2 = xyz2.permute(0, 2, 1) + + points2 = points2.permute(0, 2, 1) + B, N, C = xyz1.shape + _, S, _ = xyz2.shape + + if S == 1: + interpolated_points = points2.repeat(1, N, 1) + else: + dists = square_distance(xyz1, xyz2) + dists, idx = dists.sort(dim=-1) + dists, idx = dists[:, :, :3], idx[:, :, :3] # [B, N, 3] + + dist_recip = 1.0 / (dists + 1e-8) + norm = torch.sum(dist_recip, dim=2, keepdim=True) + weight = dist_recip / norm + interpolated_points = torch.sum(index_points(points2, idx) * weight.view(B, N, 3, 1), dim=2) + + # skip attention + if self.att is not None: + psix, psig = self.att(interpolated_points.permute(0, 2, 1), points1) + points1 = points1 * psix + + if points1 is not None: + points1 = points1.permute(0, 2, 1) + new_points = torch.cat([points1, interpolated_points], dim=-1) + else: + new_points = interpolated_points + + new_points = new_points.permute(0, 2, 1) + + for i, conv in enumerate(self.mlp_convs): + bn = self.mlp_bns[i] + new_points = F.leaky_relu(bn(conv(new_points)), 0.2) + + return new_points + + +class CIC(nn.Module): + def __init__(self, npoint, radius, k, in_channels, output_channels, bottleneck_ratio=2, mlp_num=2, curve_config=None): + super(CIC, self).__init__() + self.in_channels = in_channels + self.output_channels = output_channels + self.bottleneck_ratio = bottleneck_ratio + self.radius = radius + self.k = k + self.npoint = npoint + + planes = in_channels // bottleneck_ratio + + self.use_curve = curve_config is not None + if self.use_curve: + self.curveaggregation = CurveAggregation(planes) + self.curvegrouping = CurveGrouping(planes, k, curve_config[0], curve_config[1]) + + self.conv1 = nn.Sequential( + nn.Conv1d(in_channels, + planes, + kernel_size=1, + bias=False), + nn.BatchNorm1d(in_channels // bottleneck_ratio), + nn.LeakyReLU(negative_slope=0.2, inplace=True)) + + self.conv2 = nn.Sequential( + nn.Conv1d(planes, output_channels, kernel_size=1, bias=False), + nn.BatchNorm1d(output_channels)) + + if in_channels != output_channels: + self.shortcut = nn.Sequential( + nn.Conv1d(in_channels, + output_channels, + kernel_size=1, + bias=False), + nn.BatchNorm1d(output_channels)) + + self.relu = nn.LeakyReLU(negative_slope=0.2, inplace=True) + + self.maxpool = MaskedMaxPool(npoint, radius, k) + + self.lpfa = LPFA(planes, planes, k, mlp_num=mlp_num, initial=False) + + def forward(self, xyz, x): + + # max pool + if xyz.size(-1) != self.npoint: + xyz, x = self.maxpool( + xyz.transpose(1, 2).contiguous(), x) + xyz = xyz.transpose(1, 2) + + shortcut = x + x = self.conv1(x) # bs, c', n + + idx = knn(xyz, self.k, add_one_to_k=True) + + if self.use_curve: + # curve grouping + curves, flatten_curve_idxs = self.curvegrouping(x, xyz, idx[:,:,1:]) # avoid self-loop + + # curve aggregation + x = self.curveaggregation(x, curves) + else: + flatten_curve_idxs = None + + x = self.lpfa(x, xyz, idx=idx[:,:,:self.k]) #bs, c', n, k + + x = self.conv2(x) # bs, c, n + + if self.in_channels != self.output_channels: + shortcut = self.shortcut(shortcut) + + x = self.relu(x + shortcut) + return xyz, x, flatten_curve_idxs + + +class CurveAggregation(nn.Module): + def __init__(self, in_channel): + super(CurveAggregation, self).__init__() + self.in_channel = in_channel + mid_feature = in_channel // 2 + self.conva = nn.Conv1d(in_channel, + mid_feature, + kernel_size=1, + bias=False) + self.convb = nn.Conv1d(in_channel, + mid_feature, + kernel_size=1, + bias=False) + self.convc = nn.Conv1d(in_channel, + mid_feature, + kernel_size=1, + bias=False) + self.convn = nn.Conv1d(mid_feature, + mid_feature, + kernel_size=1, + bias=False) + self.convl = nn.Conv1d(mid_feature, + mid_feature, + kernel_size=1, + bias=False) + self.convd = nn.Sequential( + nn.Conv1d(mid_feature * 2, + in_channel, + kernel_size=1, + bias=False), + nn.BatchNorm1d(in_channel)) + self.line_conv_att = nn.Conv2d(in_channel, + 1, + kernel_size=1, + bias=False) + + def forward(self, x, curves): + curves_att = self.line_conv_att(curves) # bs, 1, c_n, c_l + + curver_inter = torch.sum(curves * F.softmax(curves_att, dim=-1), dim=-1) #bs, c, c_n + curves_intra = torch.sum(curves * F.softmax(curves_att, dim=-2), dim=-2) #bs, c, c_l + + curver_inter = self.conva(curver_inter) # bs, mid, n + curves_intra = self.convb(curves_intra) # bs, mid ,n + + x_logits = self.convc(x).transpose(1, 2).contiguous() + x_inter = F.softmax(torch.bmm(x_logits, curver_inter), dim=-1) # bs, n, c_n + x_intra = F.softmax(torch.bmm(x_logits, curves_intra), dim=-1) # bs, l, c_l + + + curver_inter = self.convn(curver_inter).transpose(1, 2).contiguous() + curves_intra = self.convl(curves_intra).transpose(1, 2).contiguous() + + x_inter = torch.bmm(x_inter, curver_inter) + x_intra = torch.bmm(x_intra, curves_intra) + + curve_features = torch.cat((x_inter, x_intra),dim=-1).transpose(1, 2).contiguous() + x = x + self.convd(curve_features) + + return F.leaky_relu(x, negative_slope=0.2) + + +class CurveGrouping(nn.Module): + def __init__(self, in_channel, k, curve_num, curve_length): + super(CurveGrouping, self).__init__() + self.curve_num = curve_num + self.curve_length = curve_length + self.in_channel = in_channel + self.k = k + + self.att = nn.Conv1d(in_channel, 1, kernel_size=1, bias=False) + + self.walk = Walk(in_channel, k, curve_num, curve_length) + + def forward(self, x, xyz, idx): + # starting point selection in self attention style + x_att = torch.sigmoid(self.att(x)) + x = x * x_att + + _, start_index = torch.topk(x_att, + self.curve_num, + dim=2, + sorted=False) + start_index = start_index.squeeze(1).unsqueeze(2) + + curves, flatten_curve_idxs = self.walk(xyz, x, idx, start_index) #bs, c, c_n, c_l + + return curves, flatten_curve_idxs + + +class MaskedMaxPool(nn.Module): + def __init__(self, npoint, radius, k): + super(MaskedMaxPool, self).__init__() + self.npoint = npoint + self.radius = radius + self.k = k + + def forward(self, xyz, features): + sub_xyz, neighborhood_features = sample_and_group(self.npoint, self.radius, self.k, xyz, features.transpose(1,2)) + + neighborhood_features = neighborhood_features.permute(0, 3, 1, 2).contiguous() + sub_features = F.max_pool2d( + neighborhood_features, kernel_size=[1, neighborhood_features.shape[3]] + ) # bs, c, n, 1 + sub_features = torch.squeeze(sub_features, -1) # bs, c, n + return sub_xyz, sub_features diff --git a/thirdparty/learning3d/utils/lib/build/lib.linux-x86_64-3.5/pointnet2_cuda.cpython-35m-x86_64-linux-gnu.so b/thirdparty/learning3d/utils/lib/build/lib.linux-x86_64-3.5/pointnet2_cuda.cpython-35m-x86_64-linux-gnu.so new file mode 100644 index 0000000000000000000000000000000000000000..63c56b8fa54651b4ddb3619b74c833289c000a3a --- /dev/null +++ b/thirdparty/learning3d/utils/lib/build/lib.linux-x86_64-3.5/pointnet2_cuda.cpython-35m-x86_64-linux-gnu.so @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1e364a965bd744c2e7d4e37114f03e445d9ab61718d3a72553e0fdd422d2e03c +size 7222568 diff --git a/thirdparty/learning3d/utils/lib/pointnet2.egg-info/PKG-INFO b/thirdparty/learning3d/utils/lib/pointnet2.egg-info/PKG-INFO new file mode 100644 index 0000000000000000000000000000000000000000..77535755b3e1e86ce28ba293f377fe045fbce15d --- /dev/null +++ b/thirdparty/learning3d/utils/lib/pointnet2.egg-info/PKG-INFO @@ -0,0 +1,10 @@ +Metadata-Version: 1.0 +Name: pointnet2 +Version: 0.0.0 +Summary: UNKNOWN +Home-page: UNKNOWN +Author: UNKNOWN +Author-email: UNKNOWN +License: UNKNOWN +Description: UNKNOWN +Platform: UNKNOWN diff --git a/thirdparty/learning3d/utils/lib/pointnet2.egg-info/SOURCES.txt b/thirdparty/learning3d/utils/lib/pointnet2.egg-info/SOURCES.txt new file mode 100644 index 0000000000000000000000000000000000000000..21253f64ce882771ed3a8dd2feb25cd37cc26259 --- /dev/null +++ b/thirdparty/learning3d/utils/lib/pointnet2.egg-info/SOURCES.txt @@ -0,0 +1,14 @@ +setup.py +pointnet2.egg-info/PKG-INFO +pointnet2.egg-info/SOURCES.txt +pointnet2.egg-info/dependency_links.txt +pointnet2.egg-info/top_level.txt +src/ball_query.cpp +src/ball_query_gpu.cu +src/group_points.cpp +src/group_points_gpu.cu +src/interpolate.cpp +src/interpolate_gpu.cu +src/pointnet2_api.cpp +src/sampling.cpp +src/sampling_gpu.cu \ No newline at end of file diff --git a/thirdparty/learning3d/utils/lib/pointnet2.egg-info/dependency_links.txt b/thirdparty/learning3d/utils/lib/pointnet2.egg-info/dependency_links.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/thirdparty/learning3d/utils/lib/pointnet2.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/thirdparty/learning3d/utils/lib/pointnet2.egg-info/top_level.txt b/thirdparty/learning3d/utils/lib/pointnet2.egg-info/top_level.txt new file mode 100644 index 0000000000000000000000000000000000000000..2d59a59591ec1d9290fd49300f0b42015b991a16 --- /dev/null +++ b/thirdparty/learning3d/utils/lib/pointnet2.egg-info/top_level.txt @@ -0,0 +1 @@ +pointnet2_cuda diff --git a/thirdparty/learning3d/utils/lib/pointnet2_modules.py b/thirdparty/learning3d/utils/lib/pointnet2_modules.py new file mode 100644 index 0000000000000000000000000000000000000000..5f125ce5075c738897e5f6a78c71123d0e3e44a2 --- /dev/null +++ b/thirdparty/learning3d/utils/lib/pointnet2_modules.py @@ -0,0 +1,160 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F + +from . import pointnet2_utils +from . import pytorch_utils as pt_utils +from typing import List + + +class _PointnetSAModuleBase(nn.Module): + + def __init__(self): + super().__init__() + self.npoint = None + self.groupers = None + self.mlps = None + self.pool_method = 'max_pool' + + def forward(self, xyz: torch.Tensor, features: torch.Tensor = None, new_xyz=None) -> (torch.Tensor, torch.Tensor): + """ + :param xyz: (B, N, 3) tensor of the xyz coordinates of the features + :param features: (B, N, C) tensor of the descriptors of the the features + :param new_xyz: + :return: + new_xyz: (B, npoint, 3) tensor of the new features' xyz + new_features: (B, npoint, \sum_k(mlps[k][-1])) tensor of the new_features descriptors + """ + new_features_list = [] + + xyz_flipped = xyz.transpose(1, 2).contiguous() + if new_xyz is None: + new_xyz = pointnet2_utils.gather_operation( + xyz_flipped, + pointnet2_utils.furthest_point_sample(xyz, self.npoint) + ).transpose(1, 2).contiguous() if self.npoint is not None else None + + for i in range(len(self.groupers)): + new_features = self.groupers[i](xyz, new_xyz, features) # (B, C, npoint, nsample) + + new_features = self.mlps[i](new_features) # (B, mlp[-1], npoint, nsample) + if self.pool_method == 'max_pool': + new_features = F.max_pool2d( + new_features, kernel_size=[1, new_features.size(3)] + ) # (B, mlp[-1], npoint, 1) + elif self.pool_method == 'avg_pool': + new_features = F.avg_pool2d( + new_features, kernel_size=[1, new_features.size(3)] + ) # (B, mlp[-1], npoint, 1) + else: + raise NotImplementedError + + new_features = new_features.squeeze(-1) # (B, mlp[-1], npoint) + new_features_list.append(new_features) + + return new_xyz, torch.cat(new_features_list, dim=1) + + +class PointnetSAModuleMSG(_PointnetSAModuleBase): + """Pointnet set abstraction layer with multiscale grouping""" + + def __init__(self, *, npoint: int, radii: List[float], nsamples: List[int], mlps: List[List[int]], bn: bool = True, + use_xyz: bool = True, pool_method='max_pool', instance_norm=False): + """ + :param npoint: int + :param radii: list of float, list of radii to group with + :param nsamples: list of int, number of samples in each ball query + :param mlps: list of list of int, spec of the pointnet before the global pooling for each scale + :param bn: whether to use batchnorm + :param use_xyz: + :param pool_method: max_pool / avg_pool + :param instance_norm: whether to use instance_norm + """ + super().__init__() + + assert len(radii) == len(nsamples) == len(mlps) + + self.npoint = npoint + self.groupers = nn.ModuleList() + self.mlps = nn.ModuleList() + for i in range(len(radii)): + radius = radii[i] + nsample = nsamples[i] + self.groupers.append( + pointnet2_utils.QueryAndGroup(radius, nsample, use_xyz=use_xyz) + if npoint is not None else pointnet2_utils.GroupAll(use_xyz) + ) + mlp_spec = mlps[i] + if use_xyz: + mlp_spec[0] += 3 + + self.mlps.append(pt_utils.SharedMLP(mlp_spec, bn=bn, instance_norm=instance_norm)) + self.pool_method = pool_method + + +class PointnetSAModule(PointnetSAModuleMSG): + """Pointnet set abstraction layer""" + + def __init__(self, *, mlp: List[int], npoint: int = None, radius: float = None, nsample: int = None, + bn: bool = True, use_xyz: bool = True, pool_method='max_pool', instance_norm=False): + """ + :param mlp: list of int, spec of the pointnet before the global max_pool + :param npoint: int, number of features + :param radius: float, radius of ball + :param nsample: int, number of samples in the ball query + :param bn: whether to use batchnorm + :param use_xyz: + :param pool_method: max_pool / avg_pool + :param instance_norm: whether to use instance_norm + """ + super().__init__( + mlps=[mlp], npoint=npoint, radii=[radius], nsamples=[nsample], bn=bn, use_xyz=use_xyz, + pool_method=pool_method, instance_norm=instance_norm + ) + + +class PointnetFPModule(nn.Module): + r"""Propigates the features of one set to another""" + + def __init__(self, *, mlp: List[int], bn: bool = True): + """ + :param mlp: list of int + :param bn: whether to use batchnorm + """ + super().__init__() + self.mlp = pt_utils.SharedMLP(mlp, bn=bn) + + def forward( + self, unknown: torch.Tensor, known: torch.Tensor, unknow_feats: torch.Tensor, known_feats: torch.Tensor + ) -> torch.Tensor: + """ + :param unknown: (B, n, 3) tensor of the xyz positions of the unknown features + :param known: (B, m, 3) tensor of the xyz positions of the known features + :param unknow_feats: (B, C1, n) tensor of the features to be propigated to + :param known_feats: (B, C2, m) tensor of features to be propigated + :return: + new_features: (B, mlp[-1], n) tensor of the features of the unknown features + """ + if known is not None: + dist, idx = pointnet2_utils.three_nn(unknown, known) + dist_recip = 1.0 / (dist + 1e-8) + norm = torch.sum(dist_recip, dim=2, keepdim=True) + weight = dist_recip / norm + + interpolated_feats = pointnet2_utils.three_interpolate(known_feats, idx, weight) + else: + interpolated_feats = known_feats.expand(*known_feats.size()[0:2], unknown.size(1)) + + if unknow_feats is not None: + new_features = torch.cat([interpolated_feats, unknow_feats], dim=1) # (B, C2 + C1, n) + else: + new_features = interpolated_feats + + new_features = new_features.unsqueeze(-1) + new_features = self.mlp(new_features) + + return new_features.squeeze(-1) + + +if __name__ == "__main__": + pass diff --git a/thirdparty/learning3d/utils/lib/pointnet2_utils.py b/thirdparty/learning3d/utils/lib/pointnet2_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..70c956478c5804311980281fef3165735a13f509 --- /dev/null +++ b/thirdparty/learning3d/utils/lib/pointnet2_utils.py @@ -0,0 +1,318 @@ +import torch +from torch.autograd import Variable +from torch.autograd import Function +import torch.nn as nn +from typing import Tuple + +import pointnet2_cuda as pointnet2 + + +class FurthestPointSampling(Function): + @staticmethod + def forward(ctx, xyz: torch.Tensor, npoint: int) -> torch.Tensor: + """ + Uses iterative furthest point sampling to select a set of npoint features that have the largest + minimum distance + :param ctx: + :param xyz: (B, N, 3) where N > npoint + :param npoint: int, number of features in the sampled set + :return: + output: (B, npoint) tensor containing the set + """ + assert xyz.is_contiguous() + + B, N, _ = xyz.size() + output = torch.cuda.IntTensor(B, npoint) + temp = torch.cuda.FloatTensor(B, N).fill_(1e10) + + pointnet2.furthest_point_sampling_wrapper(B, N, npoint, xyz, temp, output) + return output + + @staticmethod + def backward(xyz, a=None): + return None, None + + +furthest_point_sample = FurthestPointSampling.apply + + +class GatherOperation(Function): + + @staticmethod + def forward(ctx, features: torch.Tensor, idx: torch.Tensor) -> torch.Tensor: + """ + :param ctx: + :param features: (B, C, N) + :param idx: (B, npoint) index tensor of the features to gather + :return: + output: (B, C, npoint) + """ + assert features.is_contiguous() + assert idx.is_contiguous() + + B, npoint = idx.size() + _, C, N = features.size() + output = torch.cuda.FloatTensor(B, C, npoint) + + pointnet2.gather_points_wrapper(B, C, N, npoint, features, idx, output) + + ctx.for_backwards = (idx, C, N) + return output + + @staticmethod + def backward(ctx, grad_out): + idx, C, N = ctx.for_backwards + B, npoint = idx.size() + + grad_features = Variable(torch.cuda.FloatTensor(B, C, N).zero_()) + grad_out_data = grad_out.data.contiguous() + pointnet2.gather_points_grad_wrapper(B, C, N, npoint, grad_out_data, idx, grad_features.data) + return grad_features, None + + +gather_operation = GatherOperation.apply + +class KNN(Function): + + @staticmethod + def forward(ctx, k: int, unknown: torch.Tensor, known: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: + """ + Find the three nearest neighbors of unknown in known + :param ctx: + :param unknown: (B, N, 3) + :param known: (B, M, 3) + :return: + dist: (B, N, k) l2 distance to the three nearest neighbors + idx: (B, N, k) index of 3 nearest neighbors + """ + assert unknown.is_contiguous() + assert known.is_contiguous() + + B, N, _ = unknown.size() + m = known.size(1) + dist2 = torch.cuda.FloatTensor(B, N, k) + idx = torch.cuda.IntTensor(B, N, k) + + pointnet2.knn_wrapper(B, N, m, k, unknown, known, dist2, idx) + return torch.sqrt(dist2), idx + + @staticmethod + def backward(ctx, a=None, b=None): + return None, None, None +knn = KNN.apply + +class ThreeNN(Function): + + @staticmethod + def forward(ctx, unknown: torch.Tensor, known: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: + """ + Find the three nearest neighbors of unknown in known + :param ctx: + :param unknown: (B, N, 3) + :param known: (B, M, 3) + :return: + dist: (B, N, 3) l2 distance to the three nearest neighbors + idx: (B, N, 3) index of 3 nearest neighbors + """ + assert unknown.is_contiguous() + assert known.is_contiguous() + + B, N, _ = unknown.size() + m = known.size(1) + dist2 = torch.cuda.FloatTensor(B, N, 3) + idx = torch.cuda.IntTensor(B, N, 3) + + pointnet2.three_nn_wrapper(B, N, m, unknown, known, dist2, idx) + return torch.sqrt(dist2), idx + + @staticmethod + def backward(ctx, a=None, b=None): + return None, None + + +three_nn = ThreeNN.apply + + +class ThreeInterpolate(Function): + + @staticmethod + def forward(ctx, features: torch.Tensor, idx: torch.Tensor, weight: torch.Tensor) -> torch.Tensor: + """ + Performs weight linear interpolation on 3 features + :param ctx: + :param features: (B, C, M) Features descriptors to be interpolated from + :param idx: (B, n, 3) three nearest neighbors of the target features in features + :param weight: (B, n, 3) weights + :return: + output: (B, C, N) tensor of the interpolated features + """ + assert features.is_contiguous() + assert idx.is_contiguous() + assert weight.is_contiguous() + + B, c, m = features.size() + n = idx.size(1) + ctx.three_interpolate_for_backward = (idx, weight, m) + output = torch.cuda.FloatTensor(B, c, n) + + pointnet2.three_interpolate_wrapper(B, c, m, n, features, idx, weight, output) + return output + + @staticmethod + def backward(ctx, grad_out: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """ + :param ctx: + :param grad_out: (B, C, N) tensor with gradients of outputs + :return: + grad_features: (B, C, M) tensor with gradients of features + None: + None: + """ + idx, weight, m = ctx.three_interpolate_for_backward + B, c, n = grad_out.size() + + grad_features = Variable(torch.cuda.FloatTensor(B, c, m).zero_()) + grad_out_data = grad_out.data.contiguous() + + pointnet2.three_interpolate_grad_wrapper(B, c, n, m, grad_out_data, idx, weight, grad_features.data) + return grad_features, None, None + + +three_interpolate = ThreeInterpolate.apply + + +class GroupingOperation(Function): + + @staticmethod + def forward(ctx, features: torch.Tensor, idx: torch.Tensor) -> torch.Tensor: + """ + :param ctx: + :param features: (B, C, N) tensor of features to group + :param idx: (B, npoint, nsample) tensor containing the indicies of features to group with + :return: + output: (B, C, npoint, nsample) tensor + """ + assert features.is_contiguous() + assert idx.is_contiguous() + idx = idx.int() + B, nfeatures, nsample = idx.size() + _, C, N = features.size() + output = torch.cuda.FloatTensor(B, C, nfeatures, nsample) + + pointnet2.group_points_wrapper(B, C, N, nfeatures, nsample, features, idx, output) + + ctx.for_backwards = (idx, N) + return output + + @staticmethod + def backward(ctx, grad_out: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: + """ + :param ctx: + :param grad_out: (B, C, npoint, nsample) tensor of the gradients of the output from forward + :return: + grad_features: (B, C, N) gradient of the features + """ + idx, N = ctx.for_backwards + + B, C, npoint, nsample = grad_out.size() + grad_features = Variable(torch.cuda.FloatTensor(B, C, N).zero_()) + + grad_out_data = grad_out.data.contiguous() + pointnet2.group_points_grad_wrapper(B, C, N, npoint, nsample, grad_out_data, idx, grad_features.data) + return grad_features, None + + +grouping_operation = GroupingOperation.apply + + +class BallQuery(Function): + + @staticmethod + def forward(ctx, radius: float, nsample: int, xyz: torch.Tensor, new_xyz: torch.Tensor) -> torch.Tensor: + """ + :param ctx: + :param radius: float, radius of the balls + :param nsample: int, maximum number of features in the balls + :param xyz: (B, N, 3) xyz coordinates of the features + :param new_xyz: (B, npoint, 3) centers of the ball query + :return: + idx: (B, npoint, nsample) tensor with the indicies of the features that form the query balls + """ + assert new_xyz.is_contiguous() + assert xyz.is_contiguous() + + B, N, _ = xyz.size() + npoint = new_xyz.size(1) + idx = torch.cuda.IntTensor(B, npoint, nsample).zero_() + + pointnet2.ball_query_wrapper(B, N, npoint, radius, nsample, new_xyz, xyz, idx) + return idx + + @staticmethod + def backward(ctx, a=None): + return None, None, None, None + + +ball_query = BallQuery.apply + + +class QueryAndGroup(nn.Module): + def __init__(self, radius: float, nsample: int, use_xyz: bool = True): + """ + :param radius: float, radius of ball + :param nsample: int, maximum number of features to gather in the ball + :param use_xyz: + """ + super().__init__() + self.radius, self.nsample, self.use_xyz = radius, nsample, use_xyz + + def forward(self, xyz: torch.Tensor, new_xyz: torch.Tensor, features: torch.Tensor = None) -> Tuple[torch.Tensor]: + """ + :param xyz: (B, N, 3) xyz coordinates of the features + :param new_xyz: (B, npoint, 3) centroids + :param features: (B, C, N) descriptors of the features + :return: + new_features: (B, 3 + C, npoint, nsample) + """ + idx = ball_query(self.radius, self.nsample, xyz, new_xyz) + xyz_trans = xyz.transpose(1, 2).contiguous() + grouped_xyz = grouping_operation(xyz_trans, idx) # (B, 3, npoint, nsample) + grouped_xyz -= new_xyz.transpose(1, 2).unsqueeze(-1) + + if features is not None: + grouped_features = grouping_operation(features, idx) + if self.use_xyz: + new_features = torch.cat([grouped_xyz, grouped_features], dim=1) # (B, C + 3, npoint, nsample) + else: + new_features = grouped_features + else: + assert self.use_xyz, "Cannot have not features and not use xyz as a feature!" + new_features = grouped_xyz + + return new_features + + +class GroupAll(nn.Module): + def __init__(self, use_xyz: bool = True): + super().__init__() + self.use_xyz = use_xyz + + def forward(self, xyz: torch.Tensor, new_xyz: torch.Tensor, features: torch.Tensor = None): + """ + :param xyz: (B, N, 3) xyz coordinates of the features + :param new_xyz: ignored + :param features: (B, C, N) descriptors of the features + :return: + new_features: (B, C + 3, 1, N) + """ + grouped_xyz = xyz.transpose(1, 2).unsqueeze(2) + if features is not None: + grouped_features = features.unsqueeze(2) + if self.use_xyz: + new_features = torch.cat([grouped_xyz, grouped_features], dim=1) # (B, 3 + C, 1, N) + else: + new_features = grouped_features + else: + new_features = grouped_xyz + + return new_features diff --git a/thirdparty/learning3d/utils/lib/pytorch_utils.py b/thirdparty/learning3d/utils/lib/pytorch_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..09cb7bc76d88dde5757ac70b6e05e1e0c768cc1b --- /dev/null +++ b/thirdparty/learning3d/utils/lib/pytorch_utils.py @@ -0,0 +1,236 @@ +import torch.nn as nn +from typing import List, Tuple + + +class SharedMLP(nn.Sequential): + + def __init__( + self, + args: List[int], + *, + bn: bool = False, + activation=nn.ReLU(inplace=True), + preact: bool = False, + first: bool = False, + name: str = "", + instance_norm: bool = False, + ): + super().__init__() + + for i in range(len(args) - 1): + self.add_module( + name + 'layer{}'.format(i), + Conv2d( + args[i], + args[i + 1], + bn=(not first or not preact or (i != 0)) and bn, + activation=activation + if (not first or not preact or (i != 0)) else None, + preact=preact, + instance_norm=instance_norm + ) + ) + + +class _ConvBase(nn.Sequential): + + def __init__( + self, + in_size, + out_size, + kernel_size, + stride, + padding, + activation, + bn, + init, + conv=None, + batch_norm=None, + bias=True, + preact=False, + name="", + instance_norm=False, + instance_norm_func=None + ): + super().__init__() + + bias = bias and (not bn) + conv_unit = conv( + in_size, + out_size, + kernel_size=kernel_size, + stride=stride, + padding=padding, + bias=bias + ) + init(conv_unit.weight) + if bias: + nn.init.constant_(conv_unit.bias, 0) + + if bn: + if not preact: + bn_unit = batch_norm(out_size) + else: + bn_unit = batch_norm(in_size) + if instance_norm: + if not preact: + in_unit = instance_norm_func(out_size, affine=False, track_running_stats=False) + else: + in_unit = instance_norm_func(in_size, affine=False, track_running_stats=False) + + if preact: + if bn: + self.add_module(name + 'bn', bn_unit) + + if activation is not None: + self.add_module(name + 'activation', activation) + + if not bn and instance_norm: + self.add_module(name + 'in', in_unit) + + self.add_module(name + 'conv', conv_unit) + + if not preact: + if bn: + self.add_module(name + 'bn', bn_unit) + + if activation is not None: + self.add_module(name + 'activation', activation) + + if not bn and instance_norm: + self.add_module(name + 'in', in_unit) + + +class _BNBase(nn.Sequential): + + def __init__(self, in_size, batch_norm=None, name=""): + super().__init__() + self.add_module(name + "bn", batch_norm(in_size)) + + nn.init.constant_(self[0].weight, 1.0) + nn.init.constant_(self[0].bias, 0) + + +class BatchNorm1d(_BNBase): + + def __init__(self, in_size: int, *, name: str = ""): + super().__init__(in_size, batch_norm=nn.BatchNorm1d, name=name) + + +class BatchNorm2d(_BNBase): + + def __init__(self, in_size: int, name: str = ""): + super().__init__(in_size, batch_norm=nn.BatchNorm2d, name=name) + + +class Conv1d(_ConvBase): + + def __init__( + self, + in_size: int, + out_size: int, + *, + kernel_size: int = 1, + stride: int = 1, + padding: int = 0, + activation=nn.ReLU(inplace=True), + bn: bool = False, + init=nn.init.kaiming_normal_, + bias: bool = True, + preact: bool = False, + name: str = "", + instance_norm=False + ): + super().__init__( + in_size, + out_size, + kernel_size, + stride, + padding, + activation, + bn, + init, + conv=nn.Conv1d, + batch_norm=BatchNorm1d, + bias=bias, + preact=preact, + name=name, + instance_norm=instance_norm, + instance_norm_func=nn.InstanceNorm1d + ) + + +class Conv2d(_ConvBase): + + def __init__( + self, + in_size: int, + out_size: int, + *, + kernel_size: Tuple[int, int] = (1, 1), + stride: Tuple[int, int] = (1, 1), + padding: Tuple[int, int] = (0, 0), + activation=nn.ReLU(inplace=True), + bn: bool = False, + init=nn.init.kaiming_normal_, + bias: bool = True, + preact: bool = False, + name: str = "", + instance_norm=False + ): + super().__init__( + in_size, + out_size, + kernel_size, + stride, + padding, + activation, + bn, + init, + conv=nn.Conv2d, + batch_norm=BatchNorm2d, + bias=bias, + preact=preact, + name=name, + instance_norm=instance_norm, + instance_norm_func=nn.InstanceNorm2d + ) + + +class FC(nn.Sequential): + + def __init__( + self, + in_size: int, + out_size: int, + *, + activation=nn.ReLU(inplace=True), + bn: bool = False, + init=None, + preact: bool = False, + name: str = "" + ): + super().__init__() + + fc = nn.Linear(in_size, out_size, bias=not bn) + if init is not None: + init(fc.weight) + if not bn: + nn.init.constant(fc.bias, 0) + + if preact: + if bn: + self.add_module(name + 'bn', BatchNorm1d(in_size)) + + if activation is not None: + self.add_module(name + 'activation', activation) + + self.add_module(name + 'fc', fc) + + if not preact: + if bn: + self.add_module(name + 'bn', BatchNorm1d(out_size)) + + if activation is not None: + self.add_module(name + 'activation', activation) + diff --git a/thirdparty/learning3d/utils/lib/setup.py b/thirdparty/learning3d/utils/lib/setup.py new file mode 100644 index 0000000000000000000000000000000000000000..99e59e37b90517cc38c35d100f7f9cee0e309368 --- /dev/null +++ b/thirdparty/learning3d/utils/lib/setup.py @@ -0,0 +1,23 @@ +from setuptools import setup +from torch.utils.cpp_extension import BuildExtension, CUDAExtension + +setup( + name='pointnet2', + ext_modules=[ + CUDAExtension('pointnet2_cuda', [ + 'src/pointnet2_api.cpp', + + 'src/ball_query.cpp', + 'src/ball_query_gpu.cu', + 'src/group_points.cpp', + 'src/group_points_gpu.cu', + 'src/interpolate.cpp', + 'src/interpolate_gpu.cu', + 'src/sampling.cpp', + 'src/sampling_gpu.cu', + ], + extra_compile_args={'cxx': ['-g'], + 'nvcc': ['-O2']}) + ], + cmdclass={'build_ext': BuildExtension} +) diff --git a/thirdparty/learning3d/utils/model_common_utils.py b/thirdparty/learning3d/utils/model_common_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..1c39ef9bfb895af10836b25c9f10f0ba12541f21 --- /dev/null +++ b/thirdparty/learning3d/utils/model_common_utils.py @@ -0,0 +1,156 @@ +import torch + +def knn(x, k, add_one_to_k=False): + if add_one_to_k: k = k + 1 + inner = -2 * torch.matmul(x.transpose(2, 1).contiguous(), x) + xx = torch.sum(x**2, dim=1, keepdim=True) + pairwise_distance = -xx - inner - xx.transpose(2, 1).contiguous() + idx = pairwise_distance.topk(k=k, dim=-1)[1] # (batch_size, num_points, k) + return idx + +def pc_normalize(pc): + l = pc.shape[0] + centroid = np.mean(pc, axis=0) + pc = pc - centroid + m = np.max(np.sqrt(np.sum(pc**2, axis=1))) + pc = pc / m + return pc + +def square_distance(src, dst): + """ + Calculate Euclid distance between each two points. + src^T * dst = xn * xm + yn * ym + zn * zm; + sum(src^2, dim=-1) = xn*xn + yn*yn + zn*zn; + sum(dst^2, dim=-1) = xm*xm + ym*ym + zm*zm; + dist = (xn - xm)^2 + (yn - ym)^2 + (zn - zm)^2 + = sum(src**2,dim=-1)+sum(dst**2,dim=-1)-2*src^T*dst + Input: + src: source points, [B, N, C] + dst: target points, [B, M, C] + Output: + dist: per-point square distance, [B, N, M] + """ + B, N, _ = src.shape + _, M, _ = dst.shape + dist = -2 * torch.matmul(src, dst.permute(0, 2, 1)) + dist += torch.sum(src ** 2, -1).view(B, N, 1) + dist += torch.sum(dst ** 2, -1).view(B, 1, M) + return dist + +def index_points(points, idx): + """ + Input: + points: input points data, [B, N, C] + idx: sample index data, [B, S] + Return: + new_points:, indexed points data, [B, S, C] + """ + device = points.device + B = points.shape[0] + view_shape = list(idx.shape) + view_shape[1:] = [1] * (len(view_shape) - 1) + repeat_shape = list(idx.shape) + repeat_shape[0] = 1 + batch_indices = torch.arange(B, dtype=torch.long).to(device).view(view_shape).repeat(repeat_shape) + new_points = points[batch_indices, idx, :] + return new_points + +def farthest_point_sample(xyz, npoint, start_with_first_point=False): + """ + Input: + xyz: pointcloud data, [B, N, C] + npoint: number of samples + Return: + centroids: sampled pointcloud index, [B, npoint] + """ + device = xyz.device + B, N, C = xyz.shape + centroids = torch.zeros(B, npoint, dtype=torch.long).to(device) + distance = torch.ones(B, N).to(device) * 1e10 + if not start_with_first_point: + farthest = torch.randint(0, N, (B,), dtype=torch.long).to(device) + else: + farthest = torch.randint(0, N, (B,), dtype=torch.long).to(device) * 0 + batch_indices = torch.arange(B, dtype=torch.long).to(device) + for i in range(npoint): + centroids[:, i] = farthest + centroid = xyz[batch_indices, farthest, :].view(B, 1, 3) + dist = torch.sum((xyz - centroid) ** 2, -1) + mask = dist < distance + distance[mask] = dist[mask] + farthest = torch.max(distance, -1)[1] + return centroids + +def knn_point(k, pos1, pos2): + ''' + Input: + k: int32, number of k in k-nn search + pos1: (batch_size, ndataset, c) float32 array, input points + pos2: (batch_size, npoint, c) float32 array, query points + Output: + val: (batch_size, npoint, k) float32 array, L2 distances + idx: (batch_size, npoint, k) int32 array, indices to input points + ''' + B, N, C = pos1.shape + M = pos2.shape[1] + pos1 = pos1.view(B,1,N,-1).repeat(1,M,1,1) + pos2 = pos2.view(B,M,1,-1).repeat(1,1,N,1) + dist = torch.sum(-(pos1-pos2)**2,-1) + val,idx = dist.topk(k=k,dim = -1) + return torch.sqrt(-val), idx + +def query_ball_point(radius, nsample, xyz, new_xyz, get_cnt=False): + """ + Input: + radius: local region radius + nsample: max sample number in local region + xyz: all points, [B, N, C] + new_xyz: query points, [B, S, C] + Return: + group_idx: grouped points index, [B, S, nsample] + """ + device = xyz.device + B, N, C = xyz.shape + _, S, _ = new_xyz.shape + group_idx = torch.arange(N, dtype=torch.long).to(device).view(1, 1, N).repeat([B, S, 1]) + sqrdists = square_distance(new_xyz, xyz) + group_idx[sqrdists > radius ** 2] = N + + if get_cnt: + mask = group_idx != N + cnt = mask.sum(dim=-1) + + group_idx = group_idx.sort(dim=-1)[0][:, :, :nsample] + group_first = group_idx[:, :, 0].view(B, S, 1).repeat([1, 1, nsample]) + mask = group_idx == N + group_idx[mask] = group_first[mask] + if get_cnt: + return group_idx, cnt + else: + return group_idx + +def get_graph_feature(x, k=20, device=None): + # x = x.squeeze() + x = x.view(*x.size()[:3]) + idx = knn(x, k=k) # (batch_size, num_points, k) + batch_size, num_points, _ = idx.size() + + if device is None: + device = 'cuda' if torch.cuda.is_available() else 'cpu' + + idx_base = torch.arange(0, batch_size, device=device).view(-1, 1, 1) * num_points + + idx = idx + idx_base + + idx = idx.view(-1) + + _, num_dims, _ = x.size() + + x = x.transpose(2, 1).contiguous() # (batch_size, num_points, num_dims) -> (batch_size*num_points, num_dims) # batch_size * num_points * k + range(0, batch_size*num_points) + feature = x.view(batch_size * num_points, -1)[idx, :] + feature = feature.view(batch_size, num_points, k, num_dims) + x = x.view(batch_size, num_points, 1, num_dims).repeat(1, 1, k, 1) + + feature = torch.cat((feature, x), dim=3).permute(0, 3, 1, 2) + + return feature \ No newline at end of file diff --git a/thirdparty/learning3d/utils/pointconv_util.py b/thirdparty/learning3d/utils/pointconv_util.py new file mode 100644 index 0000000000000000000000000000000000000000..2273464a113b9a97d08e7bdfd0cfe15d10fc46e3 --- /dev/null +++ b/thirdparty/learning3d/utils/pointconv_util.py @@ -0,0 +1,382 @@ +""" +Utility function for PointConv +Originally from : https://github.com/yanx27/Pointnet_Pointnet2_pytorch/blob/master/utils.py +Modify by Wenxuan Wu +Date: September 2019 +""" +import torch +import torch.nn as nn +import torch.nn.functional as F +from time import time +import numpy as np +from sklearn.neighbors._kde import KernelDensity + +def timeit(tag, t): + print("{}: {}s".format(tag, time() - t)) + return time() + +def square_distance(src, dst): + """ + Calculate Euclid distance between each two points. + + src^T * dst = xn * xm + yn * ym + zn * zm; + sum(src^2, dim=-1) = xn*xn + yn*yn + zn*zn; + sum(dst^2, dim=-1) = xm*xm + ym*ym + zm*zm; + dist = (xn - xm)^2 + (yn - ym)^2 + (zn - zm)^2 + = sum(src**2,dim=-1)+sum(dst**2,dim=-1)-2*src^T*dst + + Input: + src: source points, [B, N, C] + dst: target points, [B, M, C] + Output: + dist: per-point square distance, [B, N, M] + """ + B, N, _ = src.shape + _, M, _ = dst.shape + dist = -2 * torch.matmul(src, dst.permute(0, 2, 1)) + dist += torch.sum(src ** 2, -1).view(B, N, 1) + dist += torch.sum(dst ** 2, -1).view(B, 1, M) + return dist + +def index_points(points, idx): + """ + + Input: + points: input points data, [B, N, C] + idx: sample index data, [B, S] + Return: + new_points:, indexed points data, [B, S, C] + """ + device = points.device + B = points.shape[0] + view_shape = list(idx.shape) + view_shape[1:] = [1] * (len(view_shape) - 1) + repeat_shape = list(idx.shape) + repeat_shape[0] = 1 + batch_indices = torch.arange(B, dtype=torch.long).to(device).view(view_shape).repeat(repeat_shape) + new_points = points[batch_indices, idx, :] + return new_points + +def farthest_point_sample(xyz, npoint): + """ + Input: + xyz: pointcloud data, [B, N, C] + npoint: number of samples + Return: + centroids: sampled pointcloud index, [B, npoint] + """ + #import ipdb; ipdb.set_trace() + device = xyz.device + B, N, C = xyz.shape + centroids = torch.zeros(B, npoint, dtype=torch.long).to(device) + distance = torch.ones(B, N).to(device) * 1e10 + #farthest = torch.randint(0, N, (B,), dtype=torch.long).to(device) + farthest = torch.zeros(B, dtype=torch.long).to(device) + batch_indices = torch.arange(B, dtype=torch.long).to(device) + for i in range(npoint): + centroids[:, i] = farthest + centroid = xyz[batch_indices, farthest, :].view(B, 1, 3) + dist = torch.sum((xyz - centroid) ** 2, -1) + mask = dist < distance + distance[mask] = dist[mask] + farthest = torch.max(distance, -1)[1] + return centroids + +def query_ball_point(radius, nsample, xyz, new_xyz): + """ + Input: + radius: local region radius + nsample: max sample number in local region + xyz: all points, [B, N, C] + new_xyz: query points, [B, S, C] + Return: + group_idx: grouped points index, [B, S, nsample] + """ + device = xyz.device + B, N, C = xyz.shape + _, S, _ = new_xyz.shape + group_idx = torch.arange(N, dtype=torch.long).to(device).view(1, 1, N).repeat([B, S, 1]) + sqrdists = square_distance(new_xyz, xyz) + group_idx[sqrdists > radius ** 2] = N + group_idx = group_idx.sort(dim=-1)[0][:, :, :nsample] + group_first = group_idx[:, :, 0].view(B, S, 1).repeat([1, 1, nsample]) + mask = group_idx == N + group_idx[mask] = group_first[mask] + return group_idx + +def knn_point(nsample, xyz, new_xyz): + """ + Input: + nsample: max sample number in local region + xyz: all points, [B, N, C] + new_xyz: query points, [B, S, C] + Return: + group_idx: grouped points index, [B, S, nsample] + """ + sqrdists = square_distance(new_xyz, xyz) + _, group_idx = torch.topk(sqrdists, nsample, dim = -1, largest=False, sorted=False) + return group_idx + +def sample_and_group(npoint, nsample, xyz, points, density_scale = None): + """ + Input: + npoint: + nsample: + xyz: input points position data, [B, N, C] + points: input points data, [B, N, D] + Return: + new_xyz: sampled points position data, [B, 1, C] + new_points: sampled points data, [B, 1, N, C+D] + """ + B, N, C = xyz.shape + S = npoint + fps_idx = farthest_point_sample(xyz, npoint) # [B, npoint, C] + new_xyz = index_points(xyz, fps_idx) + idx = knn_point(nsample, xyz, new_xyz) + grouped_xyz = index_points(xyz, idx) # [B, npoint, nsample, C] + grouped_xyz_norm = grouped_xyz - new_xyz.view(B, S, 1, C) + if points is not None: + grouped_points = index_points(points, idx) + new_points = torch.cat([grouped_xyz_norm, grouped_points], dim=-1) # [B, npoint, nsample, C+D] + else: + new_points = grouped_xyz_norm + + if density_scale is None: + return new_xyz, new_points, grouped_xyz_norm, idx + else: + grouped_density = index_points(density_scale, idx) + return new_xyz, new_points, grouped_xyz_norm, idx, grouped_density + +def sample_and_group_all(xyz, points, density_scale = None): + """ + Input: + xyz: input points position data, [B, N, C] + points: input points data, [B, N, D] + Return: + new_xyz: sampled points position data, [B, 1, C] + new_points: sampled points data, [B, 1, N, C+D] + """ + device = xyz.device + B, N, C = xyz.shape + #new_xyz = torch.zeros(B, 1, C).to(device) + new_xyz = xyz.mean(dim = 1, keepdim = True) + grouped_xyz = xyz.view(B, 1, N, C) - new_xyz.view(B, 1, 1, C) + if points is not None: + new_points = torch.cat([grouped_xyz, points.view(B, 1, N, -1)], dim=-1) + else: + new_points = grouped_xyz + if density_scale is None: + return new_xyz, new_points, grouped_xyz + else: + grouped_density = density_scale.view(B, 1, N, 1) + return new_xyz, new_points, grouped_xyz, grouped_density + +def group(nsample, xyz, points): + """ + Input: + npoint: + nsample: + xyz: input points position data, [B, N, C] + points: input points data, [B, N, D] + Return: + new_xyz: sampled points position data, [B, 1, C] + new_points: sampled points data, [B, 1, N, C+D] + """ + B, N, C = xyz.shape + S = N + new_xyz = xyz + idx = knn_point(nsample, xyz, new_xyz) + grouped_xyz = index_points(xyz, idx) # [B, npoint, nsample, C] + grouped_xyz_norm = grouped_xyz - new_xyz.view(B, S, 1, C) + if points is not None: + grouped_points = index_points(points, idx) + new_points = torch.cat([grouped_xyz_norm, grouped_points], dim=-1) # [B, npoint, nsample, C+D] + else: + new_points = grouped_xyz_norm + + return new_points, grouped_xyz_norm + +def compute_density(xyz, bandwidth): + ''' + xyz: input points position data, [B, N, C] + ''' + #import ipdb; ipdb.set_trace() + B, N, C = xyz.shape + sqrdists = square_distance(xyz, xyz) + gaussion_density = torch.exp(- sqrdists / (2.0 * bandwidth * bandwidth)) / (2.5 * bandwidth) + xyz_density = gaussion_density.mean(dim = -1) + + return xyz_density + +class DensityNet(nn.Module): + def __init__(self, hidden_unit = [16, 8]): + super(DensityNet, self).__init__() + self.mlp_convs = nn.ModuleList() + self.mlp_bns = nn.ModuleList() + + self.mlp_convs.append(nn.Conv2d(1, hidden_unit[0], 1)) + self.mlp_bns.append(nn.BatchNorm2d(hidden_unit[0])) + for i in range(1, len(hidden_unit)): + self.mlp_convs.append(nn.Conv2d(hidden_unit[i - 1], hidden_unit[i], 1)) + self.mlp_bns.append(nn.BatchNorm2d(hidden_unit[i])) + self.mlp_convs.append(nn.Conv2d(hidden_unit[-1], 1, 1)) + self.mlp_bns.append(nn.BatchNorm2d(1)) + + def forward(self, density_scale): + for i, conv in enumerate(self.mlp_convs): + bn = self.mlp_bns[i] + density_scale = bn(conv(density_scale)) + if i == len(self.mlp_convs): + density_scale = F.sigmoid(density_scale) + else: + density_scale = F.relu(density_scale) + + return density_scale + +class WeightNet(nn.Module): + + def __init__(self, in_channel, out_channel, hidden_unit = [8, 8]): + super(WeightNet, self).__init__() + + self.mlp_convs = nn.ModuleList() + self.mlp_bns = nn.ModuleList() + if hidden_unit is None or len(hidden_unit) == 0: + self.mlp_convs.append(nn.Conv2d(in_channel, out_channel, 1)) + self.mlp_bns.append(nn.BatchNorm2d(out_channel)) + else: + self.mlp_convs.append(nn.Conv2d(in_channel, hidden_unit[0], 1)) + self.mlp_bns.append(nn.BatchNorm2d(hidden_unit[0])) + for i in range(1, len(hidden_unit)): + self.mlp_convs.append(nn.Conv2d(hidden_unit[i - 1], hidden_unit[i], 1)) + self.mlp_bns.append(nn.BatchNorm2d(hidden_unit[i])) + self.mlp_convs.append(nn.Conv2d(hidden_unit[-1], out_channel, 1)) + self.mlp_bns.append(nn.BatchNorm2d(out_channel)) + + def forward(self, localized_xyz): + #xyz : BxCxKxN + + weights = localized_xyz + for i, conv in enumerate(self.mlp_convs): + bn = self.mlp_bns[i] + weights = F.relu(bn(conv(weights))) + + return weights + +class PointConvSetAbstraction(nn.Module): + def __init__(self, npoint, nsample, in_channel, mlp, group_all): + super(PointConvSetAbstraction, self).__init__() + self.npoint = npoint + self.nsample = nsample + self.mlp_convs = nn.ModuleList() + self.mlp_bns = nn.ModuleList() + last_channel = in_channel + for out_channel in mlp: + self.mlp_convs.append(nn.Conv2d(last_channel, out_channel, 1)) + self.mlp_bns.append(nn.BatchNorm2d(out_channel)) + last_channel = out_channel + + self.weightnet = WeightNet(3, 16) + self.linear = nn.Linear(16 * mlp[-1], mlp[-1]) + self.bn_linear = nn.BatchNorm1d(mlp[-1]) + self.group_all = group_all + + def forward(self, xyz, points): + """ + Input: + xyz: input points position data, [B, C, N] + points: input points data, [B, D, N] + Return: + new_xyz: sampled points position data, [B, C, S] + new_points_concat: sample points feature data, [B, D', S] + """ + B = xyz.shape[0] + xyz = xyz.permute(0, 2, 1) + if points is not None: + points = points.permute(0, 2, 1) + + if self.group_all: + new_xyz, new_points, grouped_xyz_norm = sample_and_group_all(xyz, points) + else: + new_xyz, new_points, grouped_xyz_norm, _ = sample_and_group(self.npoint, self.nsample, xyz, points) + # new_xyz: sampled points position data, [B, npoint, C] + # new_points: sampled points data, [B, npoint, nsample, C+D] + new_points = new_points.permute(0, 3, 2, 1) # [B, C+D, nsample,npoint] + for i, conv in enumerate(self.mlp_convs): + bn = self.mlp_bns[i] + new_points = F.relu(bn(conv(new_points))) + + grouped_xyz = grouped_xyz_norm.permute(0, 3, 2, 1) + weights = self.weightnet(grouped_xyz) + new_points = torch.matmul(input=new_points.permute(0, 3, 1, 2), other = weights.permute(0, 3, 2, 1)).view(B, self.npoint, -1) + new_points = self.linear(new_points) + new_points = self.bn_linear(new_points.permute(0, 2, 1)) + new_points = F.relu(new_points) + new_xyz = new_xyz.permute(0, 2, 1) + + return new_xyz, new_points + +class PointConvDensitySetAbstraction(nn.Module): + def __init__(self, npoint, nsample, in_channel, mlp, bandwidth, group_all): + super(PointConvDensitySetAbstraction, self).__init__() + self.npoint = npoint + self.nsample = nsample + self.mlp_convs = nn.ModuleList() + self.mlp_bns = nn.ModuleList() + last_channel = in_channel + for out_channel in mlp: + self.mlp_convs.append(nn.Conv2d(last_channel, out_channel, 1)) + self.mlp_bns.append(nn.BatchNorm2d(out_channel)) + last_channel = out_channel + + self.weightnet = WeightNet(3, 16) + self.linear = nn.Linear(16 * mlp[-1], mlp[-1]) + self.bn_linear = nn.BatchNorm1d(mlp[-1]) + self.densitynet = DensityNet() + self.group_all = group_all + self.bandwidth = bandwidth + + def forward(self, xyz, points): + """ + Input: + xyz: input points position data, [B, C, N] + points: input points data, [B, D, N] + Return: + new_xyz: sampled points position data, [B, C, S] + new_points_concat: sample points feature data, [B, D', S] + """ + B = xyz.shape[0] + N = xyz.shape[2] + xyz = xyz.permute(0, 2, 1) + if points is not None: + points = points.permute(0, 2, 1) + + xyz_density = compute_density(xyz, self.bandwidth) + inverse_density = 1.0 / xyz_density + + if self.group_all: + new_xyz, new_points, grouped_xyz_norm, grouped_density = sample_and_group_all(xyz, points, inverse_density.view(B, N, 1)) + else: + new_xyz, new_points, grouped_xyz_norm, _, grouped_density = sample_and_group(self.npoint, self.nsample, xyz, points, inverse_density.view(B, N, 1)) + # new_xyz: sampled points position data, [B, npoint, C] + # new_points: sampled points data, [B, npoint, nsample, C+D] + new_points = new_points.permute(0, 3, 2, 1) # [B, C+D, nsample,npoint] + for i, conv in enumerate(self.mlp_convs): + bn = self.mlp_bns[i] + new_points = F.relu(bn(conv(new_points))) + + inverse_max_density = grouped_density.max(dim = 2, keepdim=True)[0] + density_scale = grouped_density / inverse_max_density + density_scale = self.densitynet(density_scale.permute(0, 3, 2, 1)) + new_points = new_points * density_scale + + grouped_xyz = grouped_xyz_norm.permute(0, 3, 2, 1) + weights = self.weightnet(grouped_xyz) + new_points = torch.matmul(input=new_points.permute(0, 3, 1, 2), other = weights.permute(0, 3, 2, 1)).view(B, self.npoint, -1) + new_points = self.linear(new_points) + new_points = self.bn_linear(new_points.permute(0, 2, 1)) + new_points = F.relu(new_points) + new_xyz = new_xyz.permute(0, 2, 1) + + return new_xyz, new_points + + \ No newline at end of file diff --git a/thirdparty/learning3d/utils/ppfnet_util.py b/thirdparty/learning3d/utils/ppfnet_util.py new file mode 100644 index 0000000000000000000000000000000000000000..c4ce228d882c419139ddf35750d00999175062fc --- /dev/null +++ b/thirdparty/learning3d/utils/ppfnet_util.py @@ -0,0 +1,244 @@ +"""Utilities for PointNet related functions + +Modified from: + Pytorch Implementation of PointNet and PointNet++ + https://github.com/yanx27/Pointnet_Pointnet2_pytorch +""" + +import torch + + +def angle_difference(src, dst): + """Calculate angle between each pair of vectors. + Assumes points are l2-normalized to unit length. + + Input: + src: source points, [B, N, C] + dst: target points, [B, M, C] + Output: + dist: per-point square distance, [B, N, M] + """ + B, N, _ = src.shape + _, M, _ = dst.shape + dist = torch.matmul(src, dst.permute(0, 2, 1)) + dist = torch.acos(dist) + + return dist + + +def square_distance(src, dst): + """Calculate Euclid distance between each two points. + src^T * dst = xn * xm + yn * ym + zn * zm; + sum(src^2, dim=-1) = xn*xn + yn*yn + zn*zn; + sum(dst^2, dim=-1) = xm*xm + ym*ym + zm*zm; + dist = (xn - xm)^2 + (yn - ym)^2 + (zn - zm)^2 + = sum(src**2,dim=-1)+sum(dst**2,dim=-1)-2*src^T*dst + + Args: + src: source points, [B, N, C] + dst: target points, [B, M, C] + Returns: + dist: per-point square distance, [B, N, M] + """ + B, N, _ = src.shape + _, M, _ = dst.shape + dist = -2 * torch.matmul(src, dst.permute(0, 2, 1)) + dist += torch.sum(src ** 2, dim=-1)[:, :, None] + dist += torch.sum(dst ** 2, dim=-1)[:, None, :] + return dist + + +def index_points(points, idx): + """Array indexing, i.e. retrieves relevant points based on indices + + Args: + points: input points data_loader, [B, N, C] + idx: sample index data_loader, [B, S]. S can be 2 dimensional + Returns: + new_points:, indexed points data_loader, [B, S, C] + """ + device = points.device + B = points.shape[0] + view_shape = list(idx.shape) + view_shape[1:] = [1] * (len(view_shape) - 1) + repeat_shape = list(idx.shape) + repeat_shape[0] = 1 + batch_indices = torch.arange(B, dtype=torch.long).to(device).view(view_shape).repeat(repeat_shape) + new_points = points[batch_indices, idx, :] + return new_points + + +def farthest_point_sample(xyz, npoint): + """Iterative farthest point sampling + + Args: + xyz: pointcloud data_loader, [B, N, C] + npoint: number of samples + Returns: + centroids: sampled pointcloud index, [B, npoint] + """ + device = xyz.device + B, N, C = xyz.shape + centroids = torch.zeros(B, npoint, dtype=torch.long).to(device) + distance = torch.ones(B, N).to(device) * 1e10 + farthest = torch.randint(0, N, (B,), dtype=torch.long).to(device) + batch_indices = torch.arange(B, dtype=torch.long).to(device) + for i in range(npoint): + centroids[:, i] = farthest + centroid = xyz[batch_indices, farthest, :].view(B, 1, 3) + dist = torch.sum((xyz - centroid) ** 2, -1) + mask = dist < distance + distance[mask] = dist[mask] + farthest = torch.max(distance, -1)[1] + return centroids + + +def query_ball_point(radius, nsample, xyz, new_xyz, itself_indices=None): + """ Grouping layer in PointNet++. + + Inputs: + radius: local region radius + nsample: max sample number in local region + xyz: all points, (B, N, C) + new_xyz: query points, (B, S, C) + itself_indices (Optional): Indices of new_xyz into xyz (B, S). + Used to try and prevent grouping the point itself into the neighborhood. + If there is insufficient points in the neighborhood, or if left is none, the resulting cluster will + still contain the center point. + Returns: + group_idx: grouped points index, [B, S, nsample] + """ + device = xyz.device + B, N, C = xyz.shape + _, S, _ = new_xyz.shape + group_idx = torch.arange(N, dtype=torch.long).to(device).view(1, 1, N).repeat([B, S, 1]) # (B, S, N) + sqrdists = square_distance(new_xyz, xyz) + + if itself_indices is not None: + # Remove indices of the center points so that it will not be chosen + batch_indices = torch.arange(B, dtype=torch.long).to(device)[:, None].repeat(1, S) # (B, S) + row_indices = torch.arange(S, dtype=torch.long).to(device)[None, :].repeat(B, 1) # (B, S) + group_idx[batch_indices, row_indices, itself_indices] = N + + group_idx[sqrdists > radius ** 2] = N + group_idx = group_idx.sort(dim=-1)[0][:, :, :nsample] + if itself_indices is not None: + group_first = itself_indices[:, :, None].repeat([1, 1, nsample]) + else: + group_first = group_idx[:, :, 0].view(B, S, 1).repeat([1, 1, nsample]) + mask = group_idx == N + group_idx[mask] = group_first[mask] + return group_idx + + +def sample_and_group(npoint: int, radius: float, nsample: int, xyz: torch.Tensor, points: torch.Tensor, + returnfps: bool=False): + """ + Args: + npoint (int): Set to negative to compute for all points + radius: + nsample: + xyz: input points position data_loader, [B, N, C] + points: input points data_loader, [B, N, D] + returnfps (bool) Whether to return furthest point indices + Returns: + new_xyz: sampled points position data_loader, [B, 1, C] + new_points: sampled points data_loader, [B, 1, N, C+D] + """ + B, N, C = xyz.shape + + if npoint > 0: + S = npoint + fps_idx = farthest_point_sample(xyz, npoint) # [B, npoint, C] + new_xyz = index_points(xyz, fps_idx) + else: + S = xyz.shape[1] + fps_idx = torch.arange(0, xyz.shape[1])[None, ...].repeat(xyz.shape[0], 1) + new_xyz = xyz + + idx = query_ball_point(radius, nsample, xyz, new_xyz) # (B, N, nsample) + grouped_xyz = index_points(xyz, idx) # (B, npoint, nsample, C) + grouped_xyz_norm = grouped_xyz - new_xyz.view(B, S, 1, C) + if points is not None: + grouped_points = index_points(points, idx) + new_points = torch.cat([grouped_xyz_norm, grouped_points], dim=-1) # [B, npoint, nsample, C+D] + else: + new_points = grouped_xyz_norm + if returnfps: + return new_xyz, new_points, grouped_xyz, fps_idx + else: + return new_xyz, new_points + + +def angle(v1: torch.Tensor, v2: torch.Tensor): + """Compute angle between 2 vectors + + For robustness, we use the same formulation as in PPFNet, i.e. + angle(v1, v2) = atan2(cross(v1, v2), dot(v1, v2)). + This handles the case where one of the vectors is 0.0, since torch.atan2(0.0, 0.0)=0.0 + + Args: + v1: (B, *, 3) + v2: (B, *, 3) + + Returns: + + """ + + cross_prod = torch.stack([v1[..., 1] * v2[..., 2] - v1[..., 2] * v2[..., 1], + v1[..., 2] * v2[..., 0] - v1[..., 0] * v2[..., 2], + v1[..., 0] * v2[..., 1] - v1[..., 1] * v2[..., 0]], dim=-1) + cross_prod_norm = torch.norm(cross_prod, dim=-1) + dot_prod = torch.sum(v1 * v2, dim=-1) + + return torch.atan2(cross_prod_norm, dot_prod) + + +def sample_and_group_multi(npoint: int, radius: float, nsample: int, xyz: torch.Tensor, normals: torch.Tensor, + returnfps: bool = False): + """Sample and group for xyz, dxyz and ppf features + + Args: + npoint(int): Number of clusters (equivalently, keypoints) to sample. + Set to negative to compute for all points + radius(int): Radius of cluster for computing local features + nsample: Maximum number of points to consider per cluster + xyz: XYZ coordinates of the points + normals: Corresponding normals for the points (required for ppf computation) + returnfps: Whether to return indices of FPS points and their neighborhood + + Returns: + Dictionary containing the following fields ['xyz', 'dxyz', 'ppf']. + If returnfps is True, also returns: grouped_xyz, fps_idx + """ + + B, N, C = xyz.shape + + if npoint > 0: + S = npoint + fps_idx = farthest_point_sample(xyz, npoint) # [B, npoint, C] + new_xyz = index_points(xyz, fps_idx) + nr = index_points(normals, fps_idx)[:, :, None, :] + else: + S = xyz.shape[1] + fps_idx = torch.arange(0, xyz.shape[1])[None, ...].repeat(xyz.shape[0], 1).to(xyz.device) + new_xyz = xyz + nr = normals[:, :, None, :] + + idx = query_ball_point(radius, nsample, xyz, new_xyz, fps_idx) # (B, npoint, nsample) + grouped_xyz = index_points(xyz, idx) # (B, npoint, nsample, C) + d = grouped_xyz - new_xyz.view(B, S, 1, C) # d = p_r - p_i (B, npoint, nsample, 3) + ni = index_points(normals, idx) + + nr_d = angle(nr, d) + ni_d = angle(ni, d) + nr_ni = angle(nr, ni) + d_norm = torch.norm(d, dim=-1) + + xyz_feat = d # (B, npoint, n_sample, 3) + ppf_feat = torch.stack([nr_d, ni_d, nr_ni, d_norm], dim=-1) # (B, npoint, n_sample, 4) + + if returnfps: + return {'xyz': new_xyz, 'dxyz': xyz_feat, 'ppf': ppf_feat}, grouped_xyz, fps_idx + else: + return {'xyz': new_xyz, 'dxyz': xyz_feat, 'ppf': ppf_feat} diff --git a/thirdparty/learning3d/utils/svd.py b/thirdparty/learning3d/utils/svd.py new file mode 100644 index 0000000000000000000000000000000000000000..63aa0ffdca79de56353fb9e0958f2be768778a25 --- /dev/null +++ b/thirdparty/learning3d/utils/svd.py @@ -0,0 +1,59 @@ +import torch +import torch.nn as nn +import math + +class SVDHead(nn.Module): + def __init__(self, emb_dims, input_shape="bnc"): + super(SVDHead, self).__init__() + self.emb_dims = emb_dims + self.reflect = nn.Parameter(torch.eye(3), requires_grad=False) + self.reflect[2, 2] = -1 + self.input_shape = input_shape + + def forward(self, *input): + src_embedding = input[0] + tgt_embedding = input[1] + src = input[2] + tgt = input[3] + batch_size = src.size(0) + if self.input_shape == "bnc": + src = src.permute(0, 2, 1) + tgt = tgt.permute(0, 2, 1) + + d_k = src_embedding.size(1) + scores = torch.matmul(src_embedding.transpose(2, 1).contiguous(), tgt_embedding) / math.sqrt(d_k) + scores = torch.softmax(scores, dim=2) + + src_corr = torch.matmul(tgt, scores.transpose(2, 1).contiguous()) + + src_centered = src - src.mean(dim=2, keepdim=True) + + src_corr_centered = src_corr - src_corr.mean(dim=2, keepdim=True) + + H = torch.matmul(src_centered, src_corr_centered.transpose(2, 1).contiguous()) + + U, S, V = [], [], [] + R = [] + + for i in range(src.size(0)): + u, s, v = torch.svd(H[i]) + r = torch.matmul(v, u.transpose(1, 0).contiguous()) + r_det = torch.det(r) + if r_det < 0: + u, s, v = torch.svd(H[i]) + v = torch.matmul(v, self.reflect) + r = torch.matmul(v, u.transpose(1, 0).contiguous()) + # r = r * self.reflect + R.append(r) + + U.append(u) + S.append(s) + V.append(v) + + U = torch.stack(U, dim=0) + V = torch.stack(V, dim=0) + S = torch.stack(S, dim=0) + R = torch.stack(R, dim=0) + + t = torch.matmul(-R, src.mean(dim=2, keepdim=True)) + src_corr.mean(dim=2, keepdim=True) + return R, t.view(batch_size, 3) \ No newline at end of file diff --git a/thirdparty/learning3d/utils/transformer.py b/thirdparty/learning3d/utils/transformer.py new file mode 100644 index 0000000000000000000000000000000000000000..2b5fad411adc3d048a65b7ea358ef101b9fb28b8 --- /dev/null +++ b/thirdparty/learning3d/utils/transformer.py @@ -0,0 +1,243 @@ +import os +import sys +import glob +import h5py +import copy +import math +import numpy as np +import torch +import torch.nn as nn +import torch.nn.functional as F + +# Part of the code is referred from: http://nlp.seas.harvard.edu/2018/04/03/attention.html#positional-encoding + +def clones(module, N): + return nn.ModuleList([copy.deepcopy(module) for _ in range(N)]) + +def attention(query, key, value, mask=None, dropout=None): + d_k = query.size(-1) + scores = torch.matmul(query, key.transpose(-2, -1).contiguous()) / math.sqrt(d_k) + if mask is not None: + scores = scores.masked_fill(mask == 0, -1e9) + p_attn = F.softmax(scores, dim=-1) + return torch.matmul(p_attn, value), p_attn + +def nearest_neighbor(src, dst): + inner = -2 * torch.matmul(src.transpose(1, 0).contiguous(), dst) # src, dst (num_dims, num_points) + distances = -torch.sum(src ** 2, dim=0, keepdim=True).transpose(1, 0).contiguous() - inner - torch.sum(dst ** 2, + dim=0, + keepdim=True) + distances, indices = distances.topk(k=1, dim=-1) + return distances, indices + + +class EncoderDecoder(nn.Module): + """ + A standard Encoder-Decoder architecture. Base for this and many + other models. + """ + + def __init__(self, encoder, decoder, src_embed, tgt_embed, generator): + super(EncoderDecoder, self).__init__() + self.encoder = encoder + self.decoder = decoder + self.src_embed = src_embed + self.tgt_embed = tgt_embed + self.generator = generator + + def forward(self, src, tgt, src_mask, tgt_mask): + "Take in and process masked src and target sequences." + return self.decode(self.encode(src, src_mask), src_mask, + tgt, tgt_mask) + + def encode(self, src, src_mask): + return self.encoder(self.src_embed(src), src_mask) + + def decode(self, memory, src_mask, tgt, tgt_mask): + return self.generator(self.decoder(self.tgt_embed(tgt), memory, src_mask, tgt_mask)) + + +class Generator(nn.Module): + def __init__(self, emb_dims): + super(Generator, self).__init__() + self.nn = nn.Sequential(nn.Linear(emb_dims, emb_dims // 2), + nn.BatchNorm1d(emb_dims // 2), + nn.ReLU(), + nn.Linear(emb_dims // 2, emb_dims // 4), + nn.BatchNorm1d(emb_dims // 4), + nn.ReLU(), + nn.Linear(emb_dims // 4, emb_dims // 8), + nn.BatchNorm1d(emb_dims // 8), + nn.ReLU()) + self.proj_rot = nn.Linear(emb_dims // 8, 4) + self.proj_trans = nn.Linear(emb_dims // 8, 3) + + def forward(self, x): + x = self.nn(x.max(dim=1)[0]) + rotation = self.proj_rot(x) + translation = self.proj_trans(x) + rotation = rotation / torch.norm(rotation, p=2, dim=1, keepdim=True) + return rotation, translation + + +class Encoder(nn.Module): + def __init__(self, layer, N): + super(Encoder, self).__init__() + self.layers = clones(layer, N) + self.norm = LayerNorm(layer.size) + + def forward(self, x, mask): + for layer in self.layers: + x = layer(x, mask) + return self.norm(x) + + +class Decoder(nn.Module): + "Generic N layer decoder with masking." + + def __init__(self, layer, N): + super(Decoder, self).__init__() + self.layers = clones(layer, N) + self.norm = LayerNorm(layer.size) + + def forward(self, x, memory, src_mask, tgt_mask): + for layer in self.layers: + x = layer(x, memory, src_mask, tgt_mask) + return self.norm(x) + + +class LayerNorm(nn.Module): + def __init__(self, features, eps=1e-6): + super(LayerNorm, self).__init__() + self.a_2 = nn.Parameter(torch.ones(features)) + self.b_2 = nn.Parameter(torch.zeros(features)) + self.eps = eps + + def forward(self, x): + mean = x.mean(-1, keepdim=True) + std = x.std(-1, keepdim=True) + return self.a_2 * (x - mean) / (std + self.eps) + self.b_2 + + +class SublayerConnection(nn.Module): + def __init__(self, size, dropout=None): + super(SublayerConnection, self).__init__() + self.norm = LayerNorm(size) + + def forward(self, x, sublayer): + return x + sublayer(self.norm(x)) + + +class EncoderLayer(nn.Module): + def __init__(self, size, self_attn, feed_forward, dropout): + super(EncoderLayer, self).__init__() + self.self_attn = self_attn + self.feed_forward = feed_forward + self.sublayer = clones(SublayerConnection(size, dropout), 2) + self.size = size + + def forward(self, x, mask): + x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask)) + return self.sublayer[1](x, self.feed_forward) + + +class DecoderLayer(nn.Module): + "Decoder is made of self-attn, src-attn, and feed forward (defined below)" + + def __init__(self, size, self_attn, src_attn, feed_forward, dropout): + super(DecoderLayer, self).__init__() + self.size = size + self.self_attn = self_attn + self.src_attn = src_attn + self.feed_forward = feed_forward + self.sublayer = clones(SublayerConnection(size, dropout), 3) + + def forward(self, x, memory, src_mask, tgt_mask): + "Follow Figure 1 (right) for connections." + m = memory + x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, tgt_mask)) + x = self.sublayer[1](x, lambda x: self.src_attn(x, m, m, src_mask)) + return self.sublayer[2](x, self.feed_forward) + + +class MultiHeadedAttention(nn.Module): + def __init__(self, h, d_model, dropout=0.1): + "Take in model size and number of heads." + super(MultiHeadedAttention, self).__init__() + assert d_model % h == 0 + # We assume d_v always equals d_k + self.d_k = d_model // h + self.h = h + self.linears = clones(nn.Linear(d_model, d_model), 4) + self.attn = None + self.dropout = None + + def forward(self, query, key, value, mask=None): + "Implements Figure 2" + if mask is not None: + # Same mask applied to all h heads. + mask = mask.unsqueeze(1) + nbatches = query.size(0) + + # 1) Do all the linear projections in batch from d_model => h x d_k + query, key, value = \ + [l(x).view(nbatches, -1, self.h, self.d_k).transpose(1, 2).contiguous() + for l, x in zip(self.linears, (query, key, value))] + + # 2) Apply attention on all the projected vectors in batch. + x, self.attn = attention(query, key, value, mask=mask, + dropout=self.dropout) + + # 3) "Concat" using a view and apply a final linear. + x = x.transpose(1, 2).contiguous() \ + .view(nbatches, -1, self.h * self.d_k) + return self.linears[-1](x) + + +class PositionwiseFeedForward(nn.Module): + "Implements FFN equation." + + def __init__(self, d_model, d_ff, dropout=0.1): + super(PositionwiseFeedForward, self).__init__() + self.w_1 = nn.Linear(d_model, d_ff) + self.norm = nn.Sequential() # nn.BatchNorm1d(d_ff) + self.w_2 = nn.Linear(d_ff, d_model) + self.dropout = None + + def forward(self, x): + return self.w_2(self.norm(F.relu(self.w_1(x)).transpose(2, 1).contiguous()).transpose(2, 1).contiguous()) + + +class Identity(nn.Module): + def __init__(self): + super(Identity, self).__init__() + + def forward(self, *input): + return input + + +class Transformer(nn.Module): + def __init__(self, emb_dims, n_blocks, dropout, ff_dims, n_heads): + super(Transformer, self).__init__() + self.emb_dims = emb_dims + self.N = n_blocks + self.dropout = dropout + self.ff_dims = ff_dims + self.n_heads = n_heads + c = copy.deepcopy + attn = MultiHeadedAttention(self.n_heads, self.emb_dims) + ff = PositionwiseFeedForward(self.emb_dims, self.ff_dims, self.dropout) + self.model = EncoderDecoder(Encoder(EncoderLayer(self.emb_dims, c(attn), c(ff), self.dropout), self.N), + Decoder(DecoderLayer(self.emb_dims, c(attn), c(attn), c(ff), self.dropout), self.N), + nn.Sequential(), + nn.Sequential(), + nn.Sequential()) + + def forward(self, *input): + src = input[0] + tgt = input[1] + src = src.transpose(2, 1).contiguous() + tgt = tgt.transpose(2, 1).contiguous() + tgt_embedding = self.model(src, tgt, None, None).transpose(2, 1).contiguous() + src_embedding = self.model(tgt, src, None, None).transpose(2, 1).contiguous() + return src_embedding, tgt_embedding \ No newline at end of file diff --git a/tools/__init__.py b/tools/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..4b6705a05c788c074cc1077d57d22042f3dc0000 --- /dev/null +++ b/tools/__init__.py @@ -0,0 +1 @@ +# Evaluation and data utilities diff --git a/tools/augmentation.py b/tools/augmentation.py new file mode 100644 index 0000000000000000000000000000000000000000..57a3b38054c8daa8d5c2b9d1e34a502024a9c0fc --- /dev/null +++ b/tools/augmentation.py @@ -0,0 +1,87 @@ +import open3d as o3 +import numpy as np +import copy + +# Set random seed for reproducibility for all libraries +np.random.seed(42) + +def apply_noise(pcd, noise_level): + ''' + This function adds gaussian noise to the point cloud by adding random values to the x, y, and z coordinates of the points. + Based on https://github.com/MIT-SPARK/TEASER-plusplus/blob/master/examples/teaser_python_ply/teaser_python_ply.py#L7 + + Args: + pcd: Open3D point cloud + noise_level (float): level of noise to add + + Returns: + pcloud: Open3D point cloud with added noise + ''' + pcloud = copy.deepcopy(pcd) + pcloud_points = np.transpose(np.asarray(pcloud.points)) + + N = pcloud_points.shape[1] + noise = (np.random.rand(3, N) - 0.5) * 2 * noise_level # gaussian noise with mean 0 and std = noise_level + pcloud_points += noise + + pcloud.points = o3.utility.Vector3dVector(pcloud_points.T) + + return pcloud + +def add_outliers(pcd, outlier_level, outlier_lowerbound, outlier_upperbound): + ''' + This function adds points to a given point cloud that have no counterpart in the other point cloud (outliers). + The outliers are generated by adding random values between the lower and upper bound to randomly chosen points in the original point cloud. + Based on https://github.com/MIT-SPARK/TEASER-plusplus/blob/master/examples/teaser_python_ply/teaser_python_ply.py#L7 + + Args: + pcd: Open3D point cloud + outlier_level: level of outliers to add + outlier_lowerbound: lower bound for the random values to add to the original points + outlier_upperbound: upper bound for the random values to add to the original points + + Returns: + pcloud: Open3D point cloud with added outliers + ''' + pcloud = copy.deepcopy(pcd) + pcloud_points = np.asarray(pcloud.points) # (number_of_points, 3) + + n_outliers = int(outlier_level/50 * pcloud_points.shape[0]) + outliers_amounts = outlier_lowerbound + np.random.rand(n_outliers) * (outlier_upperbound - outlier_lowerbound) # (outlier_upperbound - outlier_lowerbound) = the distance of the outliers from the original points. + outliers_amounts = outliers_amounts[:, np.newaxis] # reshape to (n_outliers, 1) + + original_indices = np.random.randint(0, pcloud_points.shape[0], size=n_outliers) # (n_outliers,) + outlier_points = pcloud_points[original_indices] + outliers_amounts + new_pcd = np.concatenate((pcloud_points, outlier_points)) + pcloud.points = o3.utility.Vector3dVector(new_pcd) + + return pcloud + + +def apply_occlusion(pc, radius_factor): + ''' + This function removes points from the point cloud to simulate occlusion. + The points are removed based on the distance from the camera, not randomly. + Source: https://towardsdatascience.com/3d-data-processing-with-open3d-c3062aadc72e + + Args: + pc (open3d.geometry.PointCloud): Point cloud + radius_factor (float): Factor to determine the radius for hidden point removal + + Returns: + modified_pc (open3d.geometry.PointCloud): Point cloud with occlusion + pt_map (np.array): Indices of the points that are not hidden + ''' + # Get the diameter of the target point cloud (max bound - min bound) + diameter = np.linalg.norm(np.asarray(pc.get_min_bound()) - np.asarray(pc.get_max_bound())) + + # Parameters for hidden point removal + camera = [0, 0, diameter] + radius = diameter * radius_factor # bigger radius removes fewer points + + _, pt_map = pc.hidden_point_removal(camera, radius) # returns a tuple of the point cloud and the indices of the points that are not hidden + + # Applly the hidden point removal to the target point cloud + modified_pc = pc.select_by_index(pt_map) + + return modified_pc, pt_map diff --git a/tools/data.py b/tools/data.py new file mode 100644 index 0000000000000000000000000000000000000000..cd66da677fb912fa4c114523fb33eebd41b1daaa --- /dev/null +++ b/tools/data.py @@ -0,0 +1,306 @@ +import open3d as o3 +import math +import numpy as np +import torch + +def load_point_cloud(file_path: str, as_mesh: bool = False) -> o3.geometry.PointCloud: + ''' + This function loads a point cloud from a file and returns it as a point cloud object. + If the as_mesh parameter is set to True, the point cloud is loaded as a mesh object. + + Args: + file_path (str): The path to the point cloud file + as_mesh (bool): If True, the point cloud is loaded as a mesh object + + Returns: + pcd (open3d.geometry.PointCloud): The point cloud object + ''' + if as_mesh: + # Read the mesh and convert to point cloud + mesh = o3.io.read_triangle_mesh(file_path) + if file_path.endswith('.off'): + pcd = mesh.sample_points_poisson_disk(number_of_points=10000) + else: + pcd = o3.geometry.PointCloud() + pcd.points = mesh.vertices + else: + # Read the point cloud + pcd = o3.io.read_point_cloud(file_path) + + return pcd + +def normalize_pc(pcd: o3.geometry.PointCloud, return_as_np: bool = False): + ''' + This fuction normalizes the point cloud to the range [-1, 1] (to enclose all points within a unit sphere) by centering it at the origin and scaling it. + Taken from: https://soulhackerslabs.com/normalizing-feature-scaling-point-clouds-for-machine-learning-8138c6e69f5 + + Args: + pcd (open3d.geometry.PointCloud): The input point cloud + return_as_np (bool): If True, the function returns the normalized point cloud as a numpy array + + Returns: + normalized_pcd (open3d.geometry.PointCloud): The normalized point cloud + ''' + # Convert the Open3D PointCloud to a numpy array + points = np.asarray(pcd.points) + + # Normalize the points to the range [-1, 1] + centroid = np.mean(points, axis=0) # centroid of the point cloud + points -= centroid # move the pcd to the origin + furthest_distance = np.max(np.sqrt(np.sum(abs(points)**2,axis=-1))) # furthest distance from the origin + points /= furthest_distance # scale the pcd to fit in a unit sphere + + # Covert the normalized points back to Open3D PointCloud + normalized_pcd = o3.geometry.PointCloud() + normalized_pcd.points = o3.utility.Vector3dVector(points) + + if return_as_np: + return points + else: + return normalized_pcd + +def compute_distances(pcd: o3.geometry.PointCloud) -> torch.Tensor: + ''' + This function computes the distance for tasks such as normal estimation or voxel size in voxel downsampling. + + Args: + pcd (open3d.geometry.PointCloud): The input point cloud + + Returns: + distances_tensor (torch.Tensor): The distances between the points in the point cloud + ''' + distances = pcd.compute_nearest_neighbor_distance() + distances_tensor = torch.tensor(distances, dtype=torch.float32) + + return distances_tensor + +def get_normal(pcd: o3.geometry.PointCloud) -> np.ndarray: + ''' + This fucntion computes the normals of the point cloud using the KDTreeSearchParamHybrid search parameter. + + Args: + pcd (open3d.geometry.PointCloud): The input point cloud + + Returns: + normals (numpy.ndarray): The normals of the point cloud as a numpy array + ''' + max_distance = float(compute_distances(pcd).max()) + radius = max(max_distance, 0.3) # to avoid too samll or zero radius + pcd.estimate_normals(search_param=o3.geometry.KDTreeSearchParamHybrid(radius=radius, max_nn=30)) + normals = np.asarray(pcd.normals) + return normals + +def uniform_downsample(pcd: o3.geometry.PointCloud, every_k_points: int, keep_indices: bool = False) -> o3.geometry.PointCloud: + ''' + This function downsamples the point cloud uniformly by selecting every k-th point (not random). + + Args: + pcd (open3d.geometry.PointCloud): The input point cloud + every_k_points (int): Sample rate, the selected point indices are [0, k, 2k, …] + keep_indices (bool): If True, the function returns the kept indices + + Returns: + downsampled_pcd (open3d.geometry.PointCloud): The downsampled point cloud + kept_indices (list): The indices of the kept points (if keep_indices is True) + ''' + downsampled_pcd = pcd.uniform_down_sample(every_k_points) + + if keep_indices: + kept_indices = list(range(0, len(pcd.points), every_k_points)) + return downsampled_pcd, kept_indices + else: + return downsampled_pcd + +def voxel_downsample(pcd: o3.geometry.PointCloud, voxel_size: float = None, compute_normals=False) -> o3.geometry.PointCloud: + ''' + Function to downsample input pointcloud into output pointcloud with a voxel. + This is a two step process. First, it creates a voxel grid from min_bound to max_bound (think of an axis-aligned cuboid which can hold the pointcloud) + and then maps each point to the voxel that holds it. Next, averages the points belonging to same voxel. (https://github.com/isl-org/Open3D/blob/881ae76500708aec6d7d8ab070a92776334ce0cd/cpp/open3d/geometry/PointCloud.cpp#L354) + Normals and colors are averaged if they exist. + + Args: + pcd (open3d.geometry.PointCloud): The input point cloud + voxel_size (float): Voxel size for downsampling in the same unit as the pointcloud; meter, cm, feet, etc.) + compute_normals (bool): If True, the normals of the point cloud are computed, averaged and returned as numpy arrays + + Returns: + downsampled_pcd (open3d.geometry.PointCloud): The downsampled point cloud + normals (numpy.ndarray): The averaged normals of the downsampled point cloud (if compute_normals is True) + ''' + if voxel_size is None: + min_distance = float(compute_distances(pcd).min()) + voxel_size = max(min_distance * 10, 0.8) # to avoid too samll or zero voxel sizes, lower sizes might result in memory errors + else: + voxel_size = voxel_size + + if compute_normals: + max_distance = float(compute_distances(pcd).max()) + radius = max(max_distance, 0.3) # to avoid too samll or zero radius + pcd.estimate_normals(search_param=o3.geometry.KDTreeSearchParamHybrid(radius = radius, max_nn=30)) + + downsampled_pcd = pcd.voxel_down_sample(voxel_size) #averaged the normals if pcd.estimate_normals exists + + if compute_normals: + return downsampled_pcd, np.asarray(downsampled_pcd.normals) + else: + return downsampled_pcd + +def crop_point_cloud(pcd: o3.geometry.PointCloud, min_bound: tuple = (-118, -118, -118) , max_bound: tuple = (118, 118, 118)): + ''' + This function crops the point cloud to a specified bounding box defined by the minimum and maximum bounds. + Usful to get rid of obvious outliers at the edges of the point cloud / long distances from the origin. + + Args: + pcd (open3d.geometry.PointCloud): The input point cloud + min_bound (tuple): Minimum bounds of the bounding box + max_bound (tuple): Maximum bounds of the bounding box + + Returns: + cropped_pcd (open3d.geometry.PointCloud): The cropped point cloud + ''' + bbox = o3.geometry.AxisAlignedBoundingBox(min_bound, max_bound) + cropped_pcd = pcd.crop(bbox) + + return cropped_pcd + +def outlier_removal(pcd: o3.geometry.PointCloud, nb_points: int, radius: float) -> o3.geometry.PointCloud: + ''' + This function removes the outliers from the point cloud using the radius outlier removal method. + + Args: + pcd (open3d.geometry.PointCloud): The input point cloud + nb_points (int): Minimum number of points to define a neighborhood + radius (float): Radius of the sphere that will determine which points are neighbors + + Returns: + cleaned_pcd (open3d.geometry.PointCloud): The point cloud with the outliers removed + ''' + _, ind = pcd.remove_radius_outlier(nb_points, radius) + cleaned_pcd = pcd.select_by_index(ind) + + return cleaned_pcd + +def match_size(pcd1: o3.geometry.PointCloud, pcd2:o3.geometry.PointCloud) -> tuple: + ''' + This function ensures that the two point clouds have the same number of points. + + Args: + pcd1 (open3d.geometry.PointCloud): The first point cloud + pcd2 (open3d.geometry.PointCloud): The second point cloud + + Returns: + pcd1 (open3d.geometry.PointCloud): The first point cloud with the same number of points as the second point cloud + pcd2 (open3d.geometry.PointCloud): The second point cloud with the same number of points as the first point cloud + ''' + # min_len = min(len(pcd1.points), len(pcd2.points)) # change min_len to a specific length if you want to have a fixed number of points + min_len = 1441 + pcd1.points = pcd1.points[0:min_len] + pcd2.points = pcd2.points[0:min_len] + return pcd1, pcd2 + +def load_data(source_path, target_path, every_k_points, voxel_size = None , same_length = False, vdownsample=False, remove_outliers=False, compute_normals=False): + ''' + This function loads and preprocesses point clouds and optionally computers normals. + + Args: + source_path (str): Path to the source point cloud + target_path (str): Path to the target point cloud + every_k_points (int): Sample rate, the selected point indices are [0, k, 2k, …] + voxel_size (float) [optional, default = None]: Voxel size for downsampling (in the same unit as the pointcloud; meter, cm, feet, etc.). If None, the voxel size is computed as a multiple of the min distance between points in the point cloud. + same_length (bool) [optional, default = False]: If True, the target point cloud has the same number of points as the source. + vdownsample (bool) [optional, defualt = False]: If True, the point clouds are downsampled using voxel downsample + remove_outliers (bool) [optional, default = False]: If True, the outliers are removed from the point cloud + compute_normals (bool) [optional, default = False]: If True, the normals of the point clouds are computed and returned as numpy arrays + + Returns: + source (open3d.geometry.PointCloud): Source point cloud + target (open3d.geometry.PointCloud): Target point cloud + source_normals (numpy.ndarray): Normals of the source point cloud (if compute_normals is True) + target_normals (numpy.ndarray): Normals of the target point cloud (if compute_normals is True) + ''' + source = load_point_cloud(source_path) + target = load_point_cloud(target_path, as_mesh=True) + + source = crop_point_cloud(source) + + # source = normalize_pc(source) + # target = normalize_pc(target) + + + if compute_normals: + source_normals = get_normal(source) + target_normals = get_normal(target) + + if every_k_points is not None: + if compute_normals: + target, target_kept_indices = uniform_downsample(target, every_k_points, keep_indices=True) + source, source_kept_indices = uniform_downsample(source, every_k_points=math.ceil(len(source.points) / len(target.points)), keep_indices=True) + source_normals = source_normals[source_kept_indices] + target_normals = target_normals[target_kept_indices] + else: + target = uniform_downsample(target, every_k_points) + source = uniform_downsample(source, every_k_points=math.ceil(len(source.points) / len(target.points))) + + if vdownsample: + if compute_normals: + source, source_normals = voxel_downsample(source, voxel_size, compute_normals) + target, target_normals = voxel_downsample(target, voxel_size, compute_normals) + else: + source = voxel_downsample(source, voxel_size) + target = voxel_downsample(target, voxel_size) + + if remove_outliers: + source = outlier_removal(source, nb_points= 10, radius=1) + + if same_length: + source, target = match_size(source, target) + if compute_normals: + source_normals = source_normals[0:len(source.points)] + target_normals = target_normals[0:len(target.points)] + + if compute_normals: + return source, target, source_normals, target_normals + + else: + return source, target + + +def center_mass_alignment(pcd1, pcd2): + ''' + get_center() a method in Open3D that computes the centroid (geometric center) of a point cloud. + This method returns the average position of all the points in the point cloud in a numpy array with shape (3,). + The first number is the average x-coordinate, the second number is the average y-coordinate, + and the third number is the average z-coordinate of all the points in the point cloud. + ''' + pcd1_center = pcd1.get_center() + pcd2_center = pcd2.get_center() + + # Compute the translation vector + translation = pcd1_center - pcd2_center + + # Translate the target point cloud to align with the source point cloud + pcd2.translate(translation) + + return pcd1, pcd2 + +def bounding_box_alignment(pcd): + ''' + This function aligns the bounding box of the point cloud to the origin (0, 0, 0). + The bounding box is an axis-aligned bounding box (AABB) that is aligned with the x, y, and z axes. + The bounding box is defined by the minimum and maximum bounds of the point cloud in each dimension. + The translation needed to move the minimum bound to the origin is computed and applied to the point cloud. + + Args: + pcd (open3d.geometry.PointCloud): Point cloud + + Returns: + pcd (open3d.geometry.PointCloud): Point cloud with the bounding box aligned to the origin + ''' + aabb = pcd.get_axis_aligned_bounding_box() + + # Compute the translation needed to move the min bound to (0, 0, 0) + translation = -aabb.min_bound + + pcd.translate(translation) + + return pcd \ No newline at end of file diff --git a/tools/geotransformer_registration_and_evaluation.py b/tools/geotransformer_registration_and_evaluation.py new file mode 100644 index 0000000000000000000000000000000000000000..539dcc5d5aa8286c9ee925c739b8afc280edbced --- /dev/null +++ b/tools/geotransformer_registration_and_evaluation.py @@ -0,0 +1,215 @@ +import copy +import importlib.util +import sys +import time +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Optional, Tuple + +import numpy as np +import open3d as o3d +import torch + +from tools import metrics +from r3pm_net.config_loader import get_method_paths + + +@dataclass +class _GeoTransformerRunner: + geotransformer_root: Path + exp_dir: Path + weights_path: Path + device: torch.device + cfg: Any + model: torch.nn.Module + neighbor_limits: list[int] + + +_RUNNER: Optional[_GeoTransformerRunner] = None + + +def _to_device(x, device: torch.device): + """Recursively move tensors to a device (CPU or CUDA).""" + if isinstance(x, dict): + return {k: _to_device(v, device) for k, v in x.items()} + if isinstance(x, list): + return [_to_device(v, device) for v in x] + if isinstance(x, tuple): + return tuple(_to_device(v, device) for v in x) + if torch.is_tensor(x): + return x.to(device) + return x + + +def _init_runner( + geotransformer_root: Path, + exp_dir: Path, + weights_path: Path, + *, + device: Optional[str | torch.device] = None, + neighbor_limits: Optional[list[int]] = None, +) -> _GeoTransformerRunner: + # Ensure GeoTransformer is importable without installation. + # IMPORTANT: do NOT use `from config import ...` / `from model import ...` here because + # other runners (e.g. PARENet) also import `config` and `model`, and Python caches them + # in `sys.modules`. That can cause accidental cross-imports. + if str(exp_dir) not in sys.path: + sys.path.insert(0, str(exp_dir)) + if str(geotransformer_root) not in sys.path: + sys.path.insert(0, str(geotransformer_root)) + + if device is None: + device_t = torch.device("cuda" if torch.cuda.is_available() else "cpu") + else: + device_t = device if isinstance(device, torch.device) else torch.device(device) + + def _load_module(mod_name: str, file_path: Path): + spec = importlib.util.spec_from_file_location(mod_name, str(file_path)) + if spec is None or spec.loader is None: + raise ImportError(f"Failed to load module spec for {file_path}") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + # Load experiment `config.py` / `model.py` by file path with unique names (avoid collisions). + cfg_mod = _load_module(f"_geotransformer_cfg_{exp_dir.name}", exp_dir / "config.py") + + prev_backbone = sys.modules.get("backbone") + try: + sys.modules["backbone"] = _load_module(f"_geotransformer_backbone_{exp_dir.name}", exp_dir / "backbone.py") + model_mod = _load_module(f"_geotransformer_model_{exp_dir.name}", exp_dir / "model.py") + finally: + if prev_backbone is None: + sys.modules.pop("backbone", None) + else: + sys.modules["backbone"] = prev_backbone + + cfg = cfg_mod.make_cfg() + + if neighbor_limits is None: + neighbor_limits = [256] * int(cfg.backbone.num_stages) + if len(neighbor_limits) != int(cfg.backbone.num_stages): + raise ValueError( + f"GeoTransformer neighbor_limits must have length {cfg.backbone.num_stages}, got {len(neighbor_limits)}" + ) + + model = model_mod.create_model(cfg).to(device_t) + state = torch.load(str(weights_path), map_location=device_t) + state_dict = state["model"] if isinstance(state, dict) and "model" in state else state + try: + model.load_state_dict(state_dict, strict=True) + except RuntimeError: + # Be permissive if checkpoint key names differ slightly. + model.load_state_dict(state_dict, strict=False) + model.eval() + + return _GeoTransformerRunner( + geotransformer_root=geotransformer_root, + exp_dir=exp_dir, + weights_path=weights_path, + device=device_t, + cfg=cfg, + model=model, + neighbor_limits=neighbor_limits, + ) + + +def geotransformer_reg_and_eval( + source: "o3d.geometry.PointCloud", + target: "o3d.geometry.PointCloud", + *, + gt_transformation: Optional[np.ndarray] = None, + geotransformer_root: str | Path = "/home/ykashefbahrami/GeoTransformer", + exp_subdir: str = "experiments/geotransformer.modelnet.rpmnet.stage4.gse.k3.max.oacl.stage2.sinkhorn", + weights_path: str | Path = "/home/ykashefbahrami/GeoTransformer/weights/geotransformer-modelnet.pth.tar", + neighbor_limits: Optional[list[int]] = None, + device: Optional[str | torch.device] = None, +) -> Tuple["o3d.geometry.PointCloud", tuple]: + """ + Run GeoTransformer on a (source, target) pair and evaluate using this repo's `common.metrics`. + + Notes: + - GeoTransformer expects `data_dict["transform"]` mapping src -> ref. If `gt_transformation` + is not provided, we pass identity (this allows no-GT evaluation). + - The returned `eval_results` matches `metrics.all_evaluations(...)`. + """ + global _RUNNER + + geotransformer_root_p = Path(geotransformer_root).resolve() + exp_dir = (geotransformer_root_p / exp_subdir).resolve() + weights_path_p = Path(weights_path).resolve() + + if not exp_dir.exists(): + raise FileNotFoundError(f"GeoTransformer experiment directory not found: {exp_dir}") + if not weights_path_p.exists(): + raise FileNotFoundError(f"GeoTransformer weights not found: {weights_path_p}") + + if ( + _RUNNER is None + or _RUNNER.exp_dir != exp_dir + or _RUNNER.weights_path != weights_path_p + or (neighbor_limits is not None and _RUNNER.neighbor_limits != neighbor_limits) + ): + _RUNNER = _init_runner( + geotransformer_root_p, + exp_dir, + weights_path_p, + device=device, + neighbor_limits=neighbor_limits, + ) + + from geotransformer.utils.data import registration_collate_fn_stack_mode + + src_points = np.asarray(source.points, dtype=np.float32) + ref_points = np.asarray(target.points, dtype=np.float32) + src_feats = np.ones((src_points.shape[0], 1), dtype=np.float32) + ref_feats = np.ones((ref_points.shape[0], 1), dtype=np.float32) + + data_dict = { + "ref_points": ref_points, + "src_points": src_points, + "ref_feats": ref_feats, + "src_feats": src_feats, + } + if gt_transformation is None: + data_dict["transform"] = np.eye(4, dtype=np.float32) + else: + data_dict["transform"] = np.asarray(gt_transformation, dtype=np.float32) + + batch = registration_collate_fn_stack_mode( + [data_dict], + _RUNNER.cfg.backbone.num_stages, + _RUNNER.cfg.backbone.init_voxel_size, + _RUNNER.cfg.backbone.init_radius, + _RUNNER.neighbor_limits, + ) + batch = _to_device(batch, _RUNNER.device) + + # Warm-up (avoid slow first run) + with torch.no_grad(): + _RUNNER.model(batch) + + start = time.time() + with torch.no_grad(): + output = _RUNNER.model(batch) + end = time.time() + + est = output["estimated_transform"].detach().cpu().numpy() + if est.shape == (1, 4, 4): + est = est[0] + if est.shape != (4, 4): + raise ValueError(f"Unexpected GeoTransformer estimated_transform shape: {est.shape}") + est = est.astype(np.float64) + + pc_result = copy.deepcopy(source).transform(est) + eval_results = metrics.all_evaluations( + source, + target, + pc_result, + end - start, + gt_transformation=gt_transformation, + est_transformation=est, + corres=None, + ) + return pc_result, eval_results + diff --git a/tools/icp_registration_and_evaluation.py b/tools/icp_registration_and_evaluation.py new file mode 100644 index 0000000000000000000000000000000000000000..6649c38232f98b25f30cb553d4658534cd248645 --- /dev/null +++ b/tools/icp_registration_and_evaluation.py @@ -0,0 +1,67 @@ +import time +import copy +import open3d as o3d + +from tools import metrics + +def icp_reg_and_eval(source, target, method, max_correspondence_distance, init_transformation, gt_transformation): + """ + Perform registration and evaluation for a given ICP-based method. + + Args: + source (open3d.geometry.PointCloud): The source point cloud. + target (open3d.geometry.PointCloud): The target point cloud. + method (str): The registration method ('p2point', 'p2plane', 'robust', 'gicp', 'fgr'). + max_correspondence_distance (float): The maximum correspondence distance. + init_transformation (np.ndarray): The initial transformation matrix. + gt_transformation (np.ndarray): The ground truth transformation matrix. + + Returns: + result (np.ndarray): The evaluation results (rmse, rotation_error, translation_error, computation_time). + """ + if method == 'p2point': + start_time = time.time() + result = o3d.pipelines.registration.registration_icp( + source, target, max_correspondence_distance, init_transformation, + o3d.pipelines.registration.TransformationEstimationPointToPoint()) + end_time = time.time() + computation_time = end_time - start_time + + elif method == 'p2plane': + source.estimate_normals(search_param=o3d.geometry.KDTreeSearchParamHybrid(radius=0.1, max_nn=30)) + target.estimate_normals(search_param=o3d.geometry.KDTreeSearchParamHybrid(radius=0.1, max_nn=30)) + start_time = time.time() + result = o3d.pipelines.registration.registration_icp( + source, target, max_correspondence_distance, init_transformation, + o3d.pipelines.registration.TransformationEstimationPointToPlane()) + end_time = time.time() + computation_time = end_time - start_time + elif method == 'robust': + loss = o3d.pipelines.registration.TukeyLoss(k=0.5) + start_time = time.time() + result = o3d.pipelines.registration.registration_icp( + source, target, max_correspondence_distance, init_transformation, + o3d.pipelines.registration.TransformationEstimationPointToPlane(loss)) + end_time = time.time() + computation_time = end_time - start_time + elif method == 'gicp': + # Define convergence criteria + criteria = o3d.pipelines.registration.ICPConvergenceCriteria( + relative_fitness=1e-6, relative_rmse=1e-6, max_iteration=50) #default: relative_fitness=1e-6, relative_rmse=1e-6, max_iteration=30 + estimation_method = o3d.pipelines.registration.TransformationEstimationForGeneralizedICP(epsilon=0.001) + + start_time = time.time() + result = o3d.pipelines.registration.registration_generalized_icp( + source, target, max_correspondence_distance, init_transformation,estimation_method, criteria) + end_time = time.time() + computation_time = end_time - start_time + else: + raise ValueError(f"Unknown method: {method}") + + # Apply transformation + pc_result = copy.deepcopy(source).transform(result.transformation) + + # Evaluation + evaluation_results = metrics.all_evaluations(source, target, pc_result, computation_time, gt_transformation, result, corres= None) + + return pc_result, evaluation_results \ No newline at end of file diff --git a/tools/l3d_helper.py b/tools/l3d_helper.py new file mode 100644 index 0000000000000000000000000000000000000000..e6aae01d33e7bb068d3b0321d555ba44215aab03 --- /dev/null +++ b/tools/l3d_helper.py @@ -0,0 +1,152 @@ +import argparse +import os +import sys + +import numpy as np +import torch + +from r3pm_net.paths import REPO_ROOT + +# Default pretrained layout: place learning3d-style checkpoints under this tree (see README). +pretained_base_dir = os.environ.get( + "R3PM_NET_PRETRAINED_ROOT", str(REPO_ROOT / "checkpoints" / "pretrained") +) + + +def convert_data(pcd, device): + pcd_mat = np.asarray(pcd.points) + pcd_tensor = np.zeros((1, pcd_mat.shape[0], 3)) + pcd_tensor[0, :, :] = pcd_mat + torch_tensor = torch.from_numpy(pcd_tensor) + torch_tensor = torch_tensor.to(device=device, dtype=torch.float) + return torch_tensor + + +def options(modelName): + parser = argparse.ArgumentParser(description="Point Cloud Registration") + + if modelName == "DCP": + parser.add_argument( + "--pointnet", + default="tune", + type=str, + choices=["fixed", "tune"], + help="train pointnet (default: tune)", + ) + parser.add_argument( + "--emb_dims", + default=512, + type=int, + metavar="K", + help="dim. of the feature vector (default: 1024)", + ) + parser.add_argument( + "--symfn", default="max", choices=["max", "avg"], help="symmetric function (default: max)" + ) + parser.add_argument( + "--pretrained", + default=os.path.join(pretained_base_dir, "exp_dcp/models/best_model.t7"), + type=str, + metavar="PATH", + help="path to pretrained model file (default: null (no-use))", + ) + + elif modelName == "RPMNet": + parser.add_argument( + "--pretrained", + default=os.path.join(pretained_base_dir, "exp_rpmnet/models/clean-trained.pth"), + type=str, + metavar="PATH", + help="path to pretrained model file (default: null (no-use))", + ) + + elif modelName == "R3PMNet": + parser.add_argument( + "--pretrained", + default=os.path.join(pretained_base_dir, "exp_rpmnet/models/clean-trained.pth"), + type=str, + metavar="PATH", + help="path to pretrained model file (default: null (no-use))", + ) + + elif modelName == "PCRNet": + parser.add_argument( + "--emb_dims", + default=1024, + type=int, + metavar="K", + help="dim. of the feature vector (default: 1024)", + ) + parser.add_argument( + "--symfn", default="max", choices=["max", "avg"], help="symmetric function (default: max)" + ) + parser.add_argument( + "--pretrained", + default=os.path.join(pretained_base_dir, "exp_ipcrnet/models/best_model.t7"), + type=str, + metavar="PATH", + help="path to pretrained model file (default: null (no-use))", + ) + + elif modelName == "PointNetLK": + parser.add_argument( + "--emb_dims", + default=1024, + type=int, + metavar="K", + help="dim. of the feature vector (default: 1024)", + ) + parser.add_argument( + "--symfn", default="max", choices=["max", "avg"], help="symmetric function (default: max)" + ) + parser.add_argument( + "--pretrained", + default=os.path.join(pretained_base_dir, "exp_pnlk/models/best_model.t7"), + type=str, + metavar="PATH", + help="path to pretrained model file (default: null (no-use))", + ) + + elif modelName == "PRNet": + parser.add_argument( + "--emb_dims", + default=512, + type=int, + metavar="K", + help="dim. of the feature vector (default: 1024)", + ) + parser.add_argument("--num_iterations", default=3, type=int, help="Number of Iterations") + parser.add_argument( + "-j", + "--workers", + default=4, + type=int, + metavar="N", + help="number of data loading workers (default: 4)", + ) + parser.add_argument( + "-b", + "--batch_size", + default=1, + type=int, + metavar="N", + help="mini-batch size (default: 32)", + ) + parser.add_argument( + "--pretrained", + default=os.path.join(pretained_base_dir, "exp_prnet/models/best_model.t7"), + type=str, + metavar="PATH", + help="path to pretrained model file (default: null (no-use))", + ) + + parser.add_argument( + "--device", default="cuda:0", type=str, metavar="DEVICE", help="use CUDA if available" + ) + + if "ipykernel" in sys.argv[0]: + args, _unknown = parser.parse_known_args([]) + else: + args, _unknown = parser.parse_known_args() + + return args diff --git a/tools/l3d_registration_and_evaluation.py b/tools/l3d_registration_and_evaluation.py new file mode 100644 index 0000000000000000000000000000000000000000..25a0ddf47fdfc93b5d1f6149eb69d2d15cf978c2 --- /dev/null +++ b/tools/l3d_registration_and_evaluation.py @@ -0,0 +1,87 @@ +import time +import copy +import os +import torch +from thirdparty.learning3d.models import PointNet, PointNetLK, DCP, DGCNN, iPCRNet, PRNet, PPFNet, RPMNet +from r3pm_net.model import R3PMNet +from r3pm_net.feature_extractor import feature_extractor # import your feature extractor here + +from tools import metrics, l3d_helper + +def l3d_reg_and_eval(source, target, method, gt_transformation, args, source_normals = None, target_normals = None): + ''' + Perform registration and evaluation for a given Lerning3d method. + + Args: + source (o3d.geometry.PointCloud): source point cloud + target (o3d.geometry.PointCloud): target point cloud + method (str): method name (e.g., 'dcp', 'rpmnet', 'pcrnet', 'pointnetlk') + gt_transformation (np.ndarray): ground truth transformation matrix + args (argparse.Namespace): arguments + + Returns: + evaluation_results (np.ndarray): The evaluation results (rmse, rotation_error, translation_error, computation_time). + ''' + + # define the model + if method == 'dcp': + dgcnn = DGCNN(emb_dims=args.emb_dims) + model = DCP(feature_model=dgcnn, cycle=True) + elif method == 'rpmnet': + model = RPMNet(feature_model=PPFNet()) + elif method == 'pcrnet': + ptnet = PointNet(emb_dims=args.emb_dims) + model = iPCRNet(feature_model=ptnet) + elif method == 'pointnetlk': + ptnet = PointNet(emb_dims=args.emb_dims, use_bn=True) + model = PointNetLK(feature_model=ptnet) + elif method == 'prnet': + model = PRNet(emb_dims=args.emb_dims, num_iters=args.num_iterations) + elif method == 'r3pmnet': + FEATURE_MODEL = feature_extractor + model = R3PMNet(feature_model=FEATURE_MODEL) + else: + raise ValueError(f"Unknown method: {method}") + + # load pretrained model + if args.pretrained: + if not os.path.isfile(args.pretrained): + raise FileNotFoundError(f"Pretrained checkpoint not found.") + try: + payload = torch.load(args.pretrained, weights_only=False) + except TypeError: + payload = torch.load(args.pretrained) + model.load_state_dict(payload, strict=False) + + # move model to device and set to eval mode + model = model.to(args.device) + model.eval() + + if source_normals is not None and target_normals is not None: + source_tensor = l3d_helper.add_normal(source, source_normals, args.device) + target_tensor = l3d_helper.add_normal(target, target_normals, args.device) + print(source_tensor.shape) + + else: + # convert data to tensor + source_tensor = l3d_helper.convert_data(source, args.device) + target_tensor = l3d_helper.convert_data(target, args.device) + + # Perform warm-up run (to avoid slow first run) + model(target_tensor, source_tensor) + + # perform registration + start_time = time.time() + output = model(target_tensor, source_tensor) + end_time = time.time() + computation_time = end_time - start_time + + # Apply transformation + result = output['est_T'].detach().cpu().numpy()[0] + result = result.reshape(4, 4) + pc_result = copy.deepcopy(source).transform(result) + + # Evaluation + evaluation_results = metrics.all_evaluations(source, target, pc_result, computation_time, gt_transformation, output, corres = None) + + return pc_result, evaluation_results \ No newline at end of file diff --git a/tools/logdesc_registration_and_evaluation.py b/tools/logdesc_registration_and_evaluation.py new file mode 100644 index 0000000000000000000000000000000000000000..2c8b6439ad643a360b0abd4d0b4cf780bd824008 --- /dev/null +++ b/tools/logdesc_registration_and_evaluation.py @@ -0,0 +1,314 @@ +import copy +import importlib +import sys +import time +from dataclasses import dataclass +from pathlib import Path +from typing import Optional, Tuple + +import numpy as np +import open3d as o3d +import torch + +from tools import metrics +from r3pm_net.config_loader import get_method_paths + + +@dataclass +class _LoGDescRunner: + logdesc_root: Path + weights_path: Path + device: torch.device + model: torch.nn.Module + sample_radius: float + max_keypoints: int + num_points_per_sample: int + + +_RUNNER: Optional[_LoGDescRunner] = None +_METHOD_CFG = get_method_paths().get("logdesc", {}) + + +def _resolve_path(root: Path, p: str | Path) -> Path: + p = Path(p) + return p if p.is_absolute() else (root / p) + + +def _kabsch_svd(P: np.ndarray, Q: np.ndarray) -> np.ndarray: + """ + Estimate rigid transform mapping P -> Q using SVD. + Returns a 4x4 matrix T where q ≈ R p + t. + """ + if P.shape != Q.shape or P.ndim != 2 or P.shape[1] != 3: + raise ValueError(f"Expected P,Q shape (N,3) equal; got {P.shape} and {Q.shape}") + + up = P.mean(axis=0) + uq = Q.mean(axis=0) + P_centered = P - up + Q_centered = Q - uq + + # Same convention as LoGDesc's `solve_icp` in `mvp_test.py`. + H = Q_centered.T @ P_centered + U, _s, Vh = np.linalg.svd(H, full_matrices=True, compute_uv=True) + R = U @ Vh + if np.linalg.det(R) < 0: + Vh[-1, :] *= -1.0 + R = U @ Vh + t = uq - (R @ up) + + T = np.eye(4, dtype=np.float64) + T[:3, :3] = R + T[:3, 3] = t + return T + + +def _init_runner( + logdesc_root: Path, + weights_path: Path, + *, + device: Optional[str | torch.device] = None, + sample_radius: float = 0.3, + max_keypoints: int = 768, + num_points_per_sample: int = 128, + sinkhorn_iterations: int = 50, + descriptor_dim: int = 132, + L: int = 6, + use_kpt: bool = False, +) -> _LoGDescRunner: + # Make LoGDesc importable without installation. + if str(logdesc_root) not in sys.path: + sys.path.insert(0, str(logdesc_root)) + + # IMPORTANT: other methods (e.g., OverlapPredator) import a top-level `models` package, + # which collides with LoGDesc's own `models/` package. If `models` is already in + # `sys.modules`, Python will not re-resolve it from the updated `sys.path`, and + # importing `models.LoGDesc_reg` will fail. + # + # We temporarily clear `models` from `sys.modules` during the import, then restore + # the previous entries to avoid breaking other runners. + models_file = logdesc_root / "models" / "LoGDesc_reg.py" + if not models_file.exists(): + raise FileNotFoundError(f"LoGDesc not found under: {logdesc_root} (missing {models_file})") + + prev_models_modules = {k: v for k, v in sys.modules.items() if k == "models" or k.startswith("models.")} + for k in list(prev_models_modules.keys()): + sys.modules.pop(k, None) + try: + LoGDesc_reg = importlib.import_module("models.LoGDesc_reg").LoGDesc_reg # type: ignore[attr-defined] + finally: + # Remove LoGDesc-loaded `models.*` entries, then restore previous ones. + new_models_modules = [k for k in list(sys.modules.keys()) if k == "models" or k.startswith("models.")] + for k in new_models_modules: + sys.modules.pop(k, None) + sys.modules.update(prev_models_modules) + + if device is None: + device_t = torch.device("cuda" if torch.cuda.is_available() else "cpu") + else: + device_t = device if isinstance(device, torch.device) else torch.device(device) + + if not weights_path.exists(): + raise FileNotFoundError(f"LoGDesc weights not found: {weights_path}") + + checkpoint = torch.load(str(weights_path), map_location=device_t) + if not isinstance(checkpoint, dict) or not checkpoint: + raise RuntimeError(f"Unexpected LoGDesc checkpoint format at: {weights_path}") + + net_cfg = { + "sinkhorn_iterations": int(sinkhorn_iterations), + "descriptor_dim": int(descriptor_dim), + "L": int(L), + "GNN_layers": ["self", "cross"], + "use_kpt": bool(use_kpt), + "lr": 1e-4, + } + + model: torch.nn.Module = LoGDesc_reg(net_cfg) + + has_module_prefix = any(str(k).startswith("module.") for k in checkpoint.keys()) + if has_module_prefix: + model = torch.nn.DataParallel(model) + + try: + model.load_state_dict(checkpoint, strict=True) + except RuntimeError: + # Fallback: try strict=False (some checkpoints differ slightly by wrapper). + model.load_state_dict(checkpoint, strict=False) + + model = model.to(device_t) + model.double().eval() + + return _LoGDescRunner( + logdesc_root=logdesc_root, + weights_path=weights_path, + device=device_t, + model=model, + sample_radius=float(sample_radius), + max_keypoints=int(max_keypoints), + num_points_per_sample=int(num_points_per_sample), + ) + + +def logdesc_reg_and_eval( + source: "o3d.geometry.PointCloud", + target: "o3d.geometry.PointCloud", + *, + gt_transformation: Optional[np.ndarray] = None, + logdesc_root: str | Path = _METHOD_CFG.get("root", "/home/ykashefbahrami/LoGDesc"), + weights_path: str | Path = _METHOD_CFG.get("weights_path", "/home/ykashefbahrami/LoGDesc/pre-trained/best_model.pth"), + device: Optional[str | torch.device] = None, + sample_radius: float = 0.3, + max_keypoints: int = 768, + num_points_per_sample: int = 128, + topk_matches: int = 128, + sinkhorn_iterations: int = 50, + descriptor_dim: int = 132, + L: int = 6, + use_kpt: bool = False, +) -> Tuple["o3d.geometry.PointCloud", tuple]: + """ + Run LoGDesc on a (source, target) pair and evaluate using this repo's `common.metrics`. + + This wrapper follows the same output contract as other runners: + returns (pc_result, eval_results) + where eval_results matches `metrics.all_evaluations(...)`. + """ + global _RUNNER + + logdesc_root_p = Path(logdesc_root).resolve() + weights_path_p = _resolve_path(logdesc_root_p, weights_path).resolve() + + if ( + _RUNNER is None + or _RUNNER.logdesc_root != logdesc_root_p + or _RUNNER.weights_path != weights_path_p + or _RUNNER.max_keypoints != int(max_keypoints) + or _RUNNER.num_points_per_sample != int(num_points_per_sample) + or abs(_RUNNER.sample_radius - float(sample_radius)) > 1e-9 + ): + _RUNNER = _init_runner( + logdesc_root_p, + weights_path_p, + device=device, + sample_radius=sample_radius, + max_keypoints=max_keypoints, + num_points_per_sample=num_points_per_sample, + sinkhorn_iterations=sinkhorn_iterations, + descriptor_dim=descriptor_dim, + L=L, + use_kpt=use_kpt, + ) + + # Import preprocessing utilities after LoGDesc root is on sys.path. + from MVP_RG.registration.dataset import get_lrfs, furthest_point_sample # type: ignore + + src_xyz = np.asarray(source.points, dtype=np.float32) + tgt_xyz = np.asarray(target.points, dtype=np.float32) + + if src_xyz.shape[0] < 16 or tgt_xyz.shape[0] < 16: + # Too few points to do anything meaningful. + est = np.eye(4, dtype=np.float64) + pc_result = copy.deepcopy(source).transform(est) + eval_results = metrics.all_evaluations( + source, + target, + pc_result, + time=0.0, + gt_transformation=gt_transformation, + est_transformation=est, + corres=None, + ) + return pc_result, eval_results + + # FPS keypoints (same as LoGDesc's dataset). + if int(max_keypoints) > 0 and src_xyz.shape[0] > int(max_keypoints): + idx0 = furthest_point_sample(src_xyz, max_points=int(max_keypoints)) + else: + idx0 = np.arange(src_xyz.shape[0]) + if int(max_keypoints) > 0 and tgt_xyz.shape[0] > int(max_keypoints): + idx1 = furthest_point_sample(tgt_xyz, max_points=int(max_keypoints)) + else: + idx1 = np.arange(tgt_xyz.shape[0]) + + kpts0 = src_xyz[idx0, :] + kpts1 = tgt_xyz[idx1, :] + + lrfs0, _patches0, _knn0, plan0, omni0, aniso0 = get_lrfs( + idx0, + src_xyz, + num_points_per_sample=int(num_points_per_sample), + sample_radius=float(sample_radius), + with_lrf=True, + ) + lrfs1, _patches1, _knn1, plan1, omni1, aniso1 = get_lrfs( + idx1, + tgt_xyz, + num_points_per_sample=int(num_points_per_sample), + sample_radius=float(sample_radius), + with_lrf=True, + ) + + batch = { + "pc0": torch.from_numpy(np.asarray(kpts0)).unsqueeze(0), + "pc1": torch.from_numpy(np.asarray(kpts1)).unsqueeze(0), + "lrfs_i": torch.from_numpy(np.asarray(lrfs0)).unsqueeze(0), + "lrfs_j": torch.from_numpy(np.asarray(lrfs1)).unsqueeze(0), + "planarity0": torch.from_numpy(np.asarray(plan0)).reshape(1, -1, 1), + "omnivariance0": torch.from_numpy(np.asarray(omni0)).reshape(1, -1, 1), + "anisotropy0": torch.from_numpy(np.asarray(aniso0)).reshape(1, -1, 1), + "planarity1": torch.from_numpy(np.asarray(plan1)).reshape(1, -1, 1), + "omnivariance1": torch.from_numpy(np.asarray(omni1)).reshape(1, -1, 1), + "anisotropy1": torch.from_numpy(np.asarray(aniso1)).reshape(1, -1, 1), + } + + # Move to device; LoGDesc internally uses double precision. + for k, v in batch.items(): + if torch.is_tensor(v): + batch[k] = v.to(_RUNNER.device) + + start = time.time() + with torch.no_grad(): + out = _RUNNER.model(batch) + end = time.time() + + # Extract matches and estimate transform. + k0 = out["keypoints0"][0].detach().cpu().numpy() + k1 = out["keypoints1"][0].detach().cpu().numpy() + matches0 = out["matches0"][0].detach().cpu().numpy().astype(np.int64) + scores0 = out["matching_scores0"][0].detach().cpu().numpy() + + valid = matches0 > -1 + mkpts0 = k0[valid] + mkpts1 = k1[matches0[valid]] + mconf = scores0[valid] + + est = np.eye(4, dtype=np.float64) + if mkpts0.shape[0] >= 3: + k = int(min(int(topk_matches), mkpts0.shape[0])) + if k <= 0: + k = mkpts0.shape[0] + if mkpts0.shape[0] > k: + # Select top-k by confidence (fast). + top_idx = np.argpartition(-mconf, kth=k - 1)[:k] + mkpts0_use = mkpts0[top_idx] + mkpts1_use = mkpts1[top_idx] + else: + mkpts0_use = mkpts0 + mkpts1_use = mkpts1 + try: + est = _kabsch_svd(mkpts0_use.astype(np.float64), mkpts1_use.astype(np.float64)) + except Exception: + est = np.eye(4, dtype=np.float64) + + pc_result = copy.deepcopy(source).transform(est) + eval_results = metrics.all_evaluations( + source, + target, + pc_result, + end - start, + gt_transformation=gt_transformation, + est_transformation=est, + corres=None, + ) + return pc_result, eval_results + diff --git a/tools/metrics.py b/tools/metrics.py new file mode 100644 index 0000000000000000000000000000000000000000..b68320c069480e7566be605dff9188171a804882 --- /dev/null +++ b/tools/metrics.py @@ -0,0 +1,596 @@ + +import open3d as o3d +import torch +import numpy as np +import matplotlib.pyplot as plt +from math import atan2 +from scipy.spatial.transform import Rotation as R +import warnings + + +def get_rotaion(est): + # Extract the rotation matrices + try: + estimated_rotation = est[:3, :3] + except TypeError: + try: + estimation_transformation = est.transformation # for open3d + except AttributeError: + try: + estimation_transformation = est.rot # for Probreg + except: # for learning 3d + detached_est = est['est_T'].detach().cpu().numpy()[0] + estimation_transformation = detached_est.reshape(4,4) + + estimated_rotation = estimation_transformation[:3, :3] + + return estimated_rotation + +def get_translation(est): + # Extract the translation vectors + try: + estimated_translation = est[:3, 3] + except TypeError: + try: + estimated_translation = est.t # for Probreg + except AttributeError: + try: + estimation_transformation = est.transformation # for open3d + except: # for learning 3d + detached_est = est['est_T'].detach().cpu().numpy()[0] + estimation_transformation = detached_est.reshape(4,4) + + estimated_translation = estimation_transformation[:3, 3] + + return estimated_translation + +def compute_rmse(result, target, corres): + ''' + This function computes the root-mean-square error (RMSE) between the (transforemed) source and target point clouds based on the correspondences. + Based on C++ code in Open3D https://github.com/isl-org/Open3D/blob/c8856fc0d4ec89f8d53591db245fd29ad946f9cb/cpp/open3d/pipelines/registration/TransformationEstimation.cpp#L20 + + args: + result (point cloud data): the transformed point cloud + target (point cloud data): the target point cloud + corres (Vector2iVector): the point-to-point correspondences between the source and target point clouds + returns: + rmse: the root-mean-square error (centimeters or meters based on the source and target point clouds) + ''' + err = 0.0 + for c in corres: + diff = np.asarray(result.points)[c[0]] - np.asarray(target.points)[c[1]] # Euclidean distance + err += np.sum(diff**2) # sum of squared distances + rmse = np.sqrt(err / len(corres)) + + return rmse + +def rotation_error(gt_transformation, est): + ''' + This function computes the rotation error as the Geodesic distance between the estimated and the ground truth rotation matrices in 3D space. + Based on formula on this page http://www.boris-belousov.net/2016/12/01/quat-dist/ + + args: + gt_transformation: the ground truth transformation (rotation matrix will be extracted from this) + est: the estimated transformation (rotation matrix will be extracted from this) + + returns: + rotation_error_deg: the rotation error (degrees) + ''' + estimated_rotation = get_rotaion(est) + ground_truth_rotation = gt_transformation[:3, :3] + + # Compute the angular distance between the two rotation matrices + R = np.dot(ground_truth_rotation, estimated_rotation.T) # Compute the relative rotation matrix (np.matmul(gt.T, est)) + trace = np.trace(R) + normalized_trace = min(max(((trace - 1) / 2), -1.0), 1.0) # Normalize and clamp the trace to avoid numerical errors + theta = np.arccos(normalized_trace) + rotation_error = abs(theta) + rotation_error_deg = np.rad2deg(rotation_error) # Convert to degrees + + return rotation_error_deg + +def translation_error(gt_transformation, est): + ''' + This function computes the translation error as the Euclidean distance between Ground Truth & Estimated translation. + Based on definitions in this paper https://arxiv.org/pdf/2103.02690 + + args: + gt_transformation: the ground truth transformation (translation vector will be extracted from this) + est: the estimated transformation (translation vector will be extracted from this) + + returns: + abs(translation_error): the absolute translation error (centimeters) + ''' + estimated_translation = get_translation(est) + ground_truth_translation = gt_transformation[:3, 3] + + # Compute the translation error + translation_error = np.linalg.norm(estimated_translation - ground_truth_translation) + + return abs(translation_error) + +def residual_error(cloud1: o3d.geometry.PointCloud, cloud2: o3d.geometry.PointCloud) -> float: + ''' + This metric combines the rotation and translation errors so that different approaches can be compared. + It uses root mean squared distance between homologous points of the source point cloud, after the execution of an algorithm, and the same point cloud at the ground truth pose. + Based on papers: + https://www.sciencedirect.com/science/article/pii/S0921889021000191?via=ihub (3.3. Error metric) (main paper) -> https://github.com/iralabdisco/point_clouds_registration_benchmark/tree/master + https://link.springer.com/article/10.1007/s11370-024-00562-1 [14] + ''' + if len(cloud1.points) != len(cloud2.points): + if len(cloud1.points) > len(cloud2.points): + cloud1.points = cloud1.points[:len(cloud2.points)] + else: + cloud2.points = cloud2.points[:len(cloud1.points)] + + assert len(cloud1.points) == len(cloud2.points), "len(cloud1.points) != len(cloud2.points)" + + centroid, _ = cloud1.compute_mean_and_covariance() + weights = np.linalg.norm(np.asarray(cloud1.points) - centroid, 2, axis=1) + distances = np.linalg.norm(np.asarray(cloud1.points) - np.asarray(cloud2.points), 2, axis=1)/len(weights) + return np.sum(distances/weights) + +def chamfer_distance(pcd1, pcd2): + ''' + Computes the Chamfer Distance between two point clouds using Open3D and PyTorch. + + The Chamfer Distance is a metric that measures the similarity between two point clouds. + It is calculated as the sum of the mean squared distances from each point in one point cloud + to its nearest neighbor in the other point cloud, in both directions. + + Args: + pcd1 (o3d.geometry.PointCloud): The first point cloud. + pcd2 (o3d.geometry.PointCloud): The second point cloud. + + Returns: + chamfer_distance (float): The Chamfer distance between the two point clouds. + ''' + dist1 = pcd1.compute_point_cloud_distance(pcd2) + dist2 = pcd2.compute_point_cloud_distance(pcd1) + dist1 = torch.tensor(np.asarray(dist1), dtype=torch.float32) + dist2 = torch.tensor(np.asarray(dist2), dtype=torch.float32) + + chamfer_distance = torch.mean(dist1) + torch.mean(dist2) + chamfer_distance = chamfer_distance.item() + + return chamfer_distance + + +def all_evaluations(source, target, result, time, gt_transformation = None, est_transformation = None, corres = None): + + cd = chamfer_distance(result, target) + error = residual_error(target, result) + computation_time = time + + max_treshold = 0.5 + inlier_rmse = o3d.pipelines.registration.evaluate_registration(result, target, max_treshold, np.eye(4)).inlier_rmse + fitness = o3d.pipelines.registration.evaluate_registration(result, target, max_treshold, np.eye(4)).fitness + + if gt_transformation is not None: + rmse = 1 + rotation_err = rotation_error(gt_transformation, est_transformation) + translation_err = translation_error(gt_transformation, est_transformation) + + return rmse, rotation_err, translation_err, computation_time, cd, error, fitness, inlier_rmse #8 + + else: + return cd, fitness, inlier_rmse, computation_time #4 + + +def summerize_results(results: np.ndarray) -> dict: + if results.shape[2] == 8: + mean_rmse = np.round(np.mean(results[:, :, 0]), 4) + mean_rotation_error = np.round(np.mean(results[:, :, 1]), 4) + mean_translation_error = np.round(np.mean(results[:, :, 2]), 4) + mean_computation_time = np.round(np.mean(results[:, :, 3]), 4) + mean_cd = np.round(np.mean(results[:, :, 4]), 4) + mean_error = np.round(np.mean(results[:, :, 5]), 4) + mean_fitness = np.round(np.mean(results[:, :, 6]), 4) + mean_inlier_rmse = np.round(np.mean(results[:, :, 7]), 4) + return { + 'mean_rmse': mean_rmse, + 'mean_rotation_error': mean_rotation_error, + 'mean_translation_error': mean_translation_error, + 'mean_computation_time': mean_computation_time, + 'mean_cd': mean_cd, + 'mean_error': mean_error, + 'mean_fitness': mean_fitness, + 'mean_inlier_rmse': mean_inlier_rmse + } + elif results.shape[2] == 5: + mean_cd = np.round(np.mean(results[:, :, 0]), 4) + mean_error = np.round(np.mean(results[:, :, 1]), 4) + mean_fitness = np.round(np.mean(results[:, :, 2]), 4) + mean_inlier_rmse = np.round(np.mean(results[:, :, 3]), 4) + mean_computation_time = np.round(np.mean(results[:, :, 4]), 4) + return { + 'mean_cd': mean_cd, + 'mean_error': mean_error, + 'mean_fitness': mean_fitness, + 'mean_inlier_rmse': mean_inlier_rmse, + 'mean_computation_time': mean_computation_time + } + else: + raise ValueError('Invalid results shape. Expected shape (N, M, 8) or (N, M, 5).') + + +def inlier_ratio(pcd, all_errors, threshold = 5): + ''' + This function calculates the inlier ratio based on a given threshold. + + args: + pcd (point cloud data): the point cloud + all_errors (list): the errors between the two point clouds (from error_histogram) + threshold (float): the threshold for inliers + + returns: + inlier_ratio (float): the ratio of inliers (set at 5 cm) + ''' + inliers = [] + for error in all_errors: + # if the error is below the threshold, the pair is an inlier + if error < threshold: + inliers.append(error) + inlier_ratio = len(inliers) / len(pcd.points) + + return inlier_ratio + + +def calculate_snr(clean_pcd, noisy_pcd): + ''' + This function calculates the signal-to-noise ratio (SNR) between two point clouds. + SNR is defined as the ratio of the RMS power of the signal (clean point cloud) to the RMS power of the noise (difference between clean and noisy point clouds). + + args: + clean_pcd (point cloud): the clean point cloud + noisy_pcd (point cloud): the noisy point cloud + + returns: + snr_db (float): the signal-to-noise ratio in decibels + ''' + # Convert point clouds to numpy arrays + clean_points = np.asarray(clean_pcd.points) + noisy_points = np.asarray(noisy_pcd.points) + + # Calculate the RMS power of the signal (clean point cloud) + signal_amplitude = np.sqrt(np.mean(np.sum(clean_points**2, axis=1))) + + # Calculate the RMS power of the noise (difference between clean and noisy point clouds) + noise_amplitude = np.sqrt(np.mean(np.sum((clean_points - noisy_points)**2, axis=1))) + + # Compute the SNR + snr = (signal_amplitude / noise_amplitude)**2 + snr_db = 10 * np.log10(snr) + + return snr_db + +def rotation_error_along_axis(gt_transformation, est_transformation, convention = 'zyx', verbose = False): + ''' + This function calculates the rotation error along the each axis (Euler angle) between the estimated and ground truth rotation matrices. + It gives a warning if gimbal lock is detected (90 or 270 degrees). + + Based on formulas and discussions in: + https://www.youtube.com/watch?v=wg9bI8-Qx2Q (10:29) + Args: + gt_transformation: the ground truth transformation + est_transformation: the estimated transformation + convention: the convention for the Euler angles (default and only one supported is 'zyx') + verbose: if True, the Euler angles are printed + + Returns: + theta_x_deg: the rotation error along the x-axis (degrees) rounded to 3 decimal places + theta_y_deg: the rotation error along the y-axis (degrees) rounded to 3 decimal places + theta_z_deg: the rotation error along the z-axis (degrees) rounded to 3 decimal places + ''' + if convention != 'zyx': + raise ValueError("Invalid convention. Only 'zyx' is supported.") + + estimated_rotation = get_rotaion(est_transformation) + ground_truth_rotation = gt_transformation[:3, :3] + + R_relative = np.dot(ground_truth_rotation, estimated_rotation.T) + + theta_z = atan2(R_relative[1, 0], R_relative[0, 0]) # Euler angles = atan2(r21, r11) + theta_x = atan2(R_relative[2, 1], R_relative[2, 2]) # Euler angles = atan2(r32, r33) + + if np.isclose(np.cos(theta_z), 0.0, atol=1e-6): + second_term = R_relative[1, 0]/np.sin(theta_z) + theta_y = atan2(-R_relative[2, 0], second_term) + else: + second_term = R_relative[0, 0]/np.cos(theta_z) + theta_y = atan2(-R_relative[2, 0], second_term) + + # Convert to degrees + theta_x_deg = np.round(abs(np.rad2deg(theta_x)), 3) + theta_y_deg = np.round(abs(np.rad2deg(theta_y)), 3) + theta_z_deg = np.round(abs(np.rad2deg(theta_z)), 3) + + if verbose: + if np.round(theta_y_deg, 3) == 90 or np.round(theta_y_deg, 3) == 270: + print("Warning: Gimbal lock detected! It might not be possible to uniquely and accurately determine all angles.") + + print(f'{theta_x_deg}° error along x-axis, {theta_y_deg}° error along y-axis and {theta_z_deg}° error along z-axis.') + + return theta_x_deg, theta_y_deg, theta_z_deg + +def angle_diff(a, b): + ''' + This function calculates the smallest unsigned angle difference in degrees. + ''' + diff = abs(a - b) % 360 + return min(diff, 360 - diff) + +def signed_angle_diff(a, b): + ''' + This function calculates the signed angle difference in degrees. + ''' + diff = (b - a + 180) % 360 - 180 + return diff + +def decompose_rotation_error(gt_transformation, est_transformation, signed = True, convention = 'zyx', verbose = False): + ''' + This fuction calculates Euler angles (rotation) differences between the estimated and ground truth transformation matrices. + It uses the scipy.spatial.transform.Rotation class to extract the Euler angles from the rotation matrices. + If Gimbal lock is detected, it uses a manual calculation of the angles. + + Args: + gt_transformation: the ground truth transformation matrix + est: the estimated transformation matrix + signed: if True, the signed angle difference is calculated + convention: the convention for the Euler angles (default and only supported is 'zyx') + This restriction is because the rotation matrix applied to simulate data is 'xyz' order which is the inverse of 'zyx' - because initrinsic and extrinsic rotations are inverted. Source: https://dominicplein.medium.com/extrinsic-intrinsic-rotation-do-i-multiply-from-right-or-left-357c38c1abfd + verbose: if True, the Euler angles are printed + Returns: + x_diff: the difference in the x-axis rotation (degrees) + y_diff: the difference in the y-axis rotation (degrees) + z_diff: the difference in the z-axis rotation (degrees) + + OR if Gimbal lock is detected: + re_along_x: the rotation error along the x-axis (degrees) + re_along_y: the rotation error along the y-axis (degrees) + re_along_z: the rotation error along the z-axis (degrees) + + Raises: + RuntimeError: if Gimbal lock is detected. + ''' + if convention != 'zyx': + raise ValueError("Invalid convention. Only 'zyx' is supported.") + + # Get rotation matrices + estimated_rotation = get_rotaion(est_transformation) + gt_rotation = gt_transformation[:3, :3] + + # Get Euler degrees using scipy + try: + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + + gt_euler = R.from_matrix(gt_rotation).as_euler(convention, degrees=True) + gt_z, gt_y, gt_x = np.round(gt_euler, 3) + est_euler = R.from_matrix(estimated_rotation).as_euler(convention, degrees=True) + est_z, est_y, est_x = np.round(est_euler, 3) + + # Check for Gimbal lock warning + gimal_lock_warning = any("Gimbal lock detected" in str(warn.message) for warn in w) + if gimal_lock_warning: + raise RuntimeError("Gimbal lock detected!") + + if verbose: + print(f"Extracted Ground Truth Euler angles (deg): x={gt_x}, y={gt_y}, z={gt_z}") + print(f"Extracted Estimated Euler angles (deg): x={est_x}, y={est_y}, z={est_z}") + + # compute differences + diff_fn = signed_angle_diff if signed else angle_diff + x_diff = diff_fn(gt_x, est_x) + y_diff = diff_fn(gt_y, est_y) + z_diff = diff_fn(gt_z, est_z) + + # Use manual calculation of angles if Gimbal lock is detected + except RuntimeError as e: + print(f'{e} - Trying manual calculation of angles.') if verbose else None + x_diff, y_diff, z_diff = rotation_error_along_axis(gt_transformation, est_transformation, convention) + + if verbose: + print(f'{x_diff}° error along x-axis, {y_diff}° error along y-axis and {z_diff}° error along z-axis.') + + return x_diff, y_diff, z_diff + + +# --------- UNUSED FUNCTIONS --------- +def error_histogram(source, target, result, dis_type ='mse', bins = 200): + ''' + This function creates a histogram of the errors between the target and result point clouds. + + args: + target: the target point cloud + result: the result point cloud + dis_type: the type of distance to calculate the error (mse or mae) + bins: the number of bins for the histogram + + returns: + histogram (Plot): the histogram plot of the errors + all_errors (list): the errors between the source and target point clouds (for inlier ratio calculation) + ''' + # Calcualte error between the correspondences + all_errors = [] + + for i in range(len(source.points)): + if dis_type =='mse': + # Error as root mean square error + error = np.sqrt(np.sum((np.asarray(result.points[i]) - np.asarray(target.points[i]))**2)) + elif dis_type == 'mae': + # Error as mean absolute error + error = np.mean(np.abs(np.asarray(result.points[i]) - np.asarray(target.points[i]))) + + all_errors.append(error) + + # Make a histogram of the errors + plt.hist(all_errors, bins = bins) + plt.xlabel('Error') + plt.ylabel('Frequency') + plt.show() + + return all_errors + + +def rotation_error_along_z (gt_transformation, est, symmetry = None): + ''' + This function calculates the rotation error along the z-axis (Euler angle) between the estimated and ground truth rotation matrices. + Based on formulas and discussions in: + https://math.stackexchange.com/questions/31001/finding-the-cos-angle-between-two-matrices-using-the-euclidean-inner-product + https://stackoverflow.com/questions/15022630/how-to-calculate-the-angle-from-rotation-matrix + https://www.youtube.com/watch?v=wg9bI8-Qx2Q (10:29) + + For C2 and C4 symmetries, the explanation is here: + https://www2.math.upenn.edu/~mlazar/math170/notes07-4.pdf + https://web.stanford.edu/~kaleeg/chem32/groupT/ + ''' + estimated_rotation = get_rotaion(est) + ground_truth_rotation = gt_transformation[:3, :3] + + R = np.dot(ground_truth_rotation, estimated_rotation.T) + theta_z = atan2(R[1, 0], R[0, 0]) # Euler angles + theta_z_deg = abs(np.rad2deg(theta_z)) + + if symmetry == 'C2': + theta_z_deg = min(theta_z_deg, abs(180 - theta_z_deg)) + elif symmetry == 'C4': + theta_z_deg = min(theta_z_deg, abs(90 - theta_z_deg), abs(180 - theta_z_deg), abs(270 - theta_z_deg)) + + return np.round(theta_z_deg, 3) + +def rotation_error_along_x (gt_transformation, est): + ''' + This function calculates the rotation error along the x-axis (Euler angle) between the estimated and ground truth rotation matrices. + Based on formulas and discussions in: + https://www.youtube.com/watch?v=wg9bI8-Qx2Q (10:29) + + Args: + est: the estimated transformation + gt_transformation: the ground truth transformation + Returns: + theta_x_deg: the rotation error along the x-axis (degrees) rounded to 3 decimal places + ''' + estimated_rotation = get_rotaion(est) + ground_truth_rotation = gt_transformation[:3, :3] + + R = np.dot(ground_truth_rotation, estimated_rotation.T) + theta_x = atan2(R[2, 1], R[2, 2]) # Euler angles = atan2(r32, r33) + theta_x_deg = abs(np.rad2deg(theta_x)) + + return np.round(theta_x_deg, 3) + +def rotation_error_along_y (gt_transformation, est, theta_z_deg): + ''' + This function calculates the rotation error along the y-axis (Euler angle) between the estimated and ground truth rotation matrices. + It gives a warning if gimbal lock is detected (90 or 270 degrees). + Based on formulas and discussions in: + https://www.youtube.com/watch?v=wg9bI8-Qx2Q + + Args: + est: the estimated transformation + gt_transformation: the ground truth transformation + theta_z_deg: the rotation error along the z-axis (degrees) + Returns: + theta_y_deg: the rotation error along the y-axis (degrees) rounded to 3 decimal places + ''' + + estimated_rotation = get_rotaion(est) + ground_truth_rotation = gt_transformation[:3, :3] + + R = np.dot(ground_truth_rotation, estimated_rotation.T) + if np.cos(np.deg2rad(theta_z_deg)) == 0: + second_term = R[1, 0]/np.sin(np.deg2rad(theta_z_deg)) + theta_y = atan2(-R[2, 0], second_term) + else: + second_term = R[0, 0]/np.cos(np.deg2rad(theta_z_deg)) + theta_y = atan2(-R[2, 0], second_term) + + theta_y_deg = abs(np.rad2deg(theta_y)) + + if np.round(theta_y_deg, 3) == 90 or np.round(theta_y_deg, 3) == 270: + print("Warning: Gimbal lock detected! It might not be possible to uniquely and accurately determine all angles.") + + return np.round(theta_y_deg, 3) + +# Construct the transformation matrix from the rotation matrix, translation vector, and scale (for Probreg) +def reconstruct_transformation_propreg(rot, t, scale): + scaled_rotation = scale * rot + T = np.eye(4) + T[:3, :3] = scaled_rotation + T[:3, 3] = t + return T + +def get_transformation(est): + # Extract the transformation matrices + try: + estimation_transformation = est.transformation # for open3d + except AttributeError: + try: + estimation_transformation = reconstruct_transformation_propreg(est.rot, est.t, est.scale) # for Probreg + except: # for learning 3d + estimation_transformation = est['est_T'].detach().cpu().numpy()[0] + estimation_transformation = estimation_transformation.reshape(4,4) + + return estimation_transformation + +def transformation_error(est, gt_transformation): # ATTENTION: not a good metric because transformations consist of rotation and translation, which have different metrics (one is radian, the other centimeters). + ''' + This function calculates transformation error as the root-mean-square error between estimated transformation and ground truth transformation + Based on definitions in this paper https://arxiv.org/pdf/2103.02690 + + args: + est: the estimation object + gt_transformation: the ground truth transformation + + returns: + transformation_error: the transformation error + ''' + estimation_transformation = get_transformation(est) + rmseT = np.sqrt(np.mean(np.square(estimation_transformation - gt_transformation))) + + return rmseT + + +def remove_outliers(data): + ''' + This function removes outliers from the data based on the interquartile range (IQR). + Based on https://medium.com/@davidnh8/outlier-detection-101-median-and-interquartile-range-cc9dde94c0ac + + Args: + data (np.array): The data to remove outliers from. + Returns: + clear_data (np.array): The data without outliers. + outlier_index (np.array): The indices of the outliers. + ''' + q1 = np.percentile(data, 25) + q3 = np.percentile(data, 75) + iqr = q3 - q1 + lower_bound = q1 - 1.5 * iqr + upper_bound = q3 + 1.5 * iqr + outlier_index = np.where((data <= lower_bound) | (data >= upper_bound)) + clear_data = np.delete(data, outlier_index) + return clear_data, outlier_index + + +def get_overlap_ratio(source,target,threshold): + """ + - Overlap is defined as the ratio of the number of points in each point cloud that cover a region of the scene, + which is also covered by the other point cloud, to the total number of points in the point cloud. + + - Overlap is computed as the ratio of the number of points in the source point cloud that are within a distance + threshold to the target point cloud to the total number of points in the source point cloud. + + Taken from https://github.com/prs-eth/OverlapPredator/blob/main/scripts/cal_overlap.py + Based on https://www.open3d.org/docs/latest/tutorial/Basic/kdtree.html + """ + pcd_tree = o3d.geometry.KDTreeFlann(target) + + match_count=0 + for i, point in enumerate(source.points): + [count, _, _] = pcd_tree.search_radius_vector_3d(point, threshold) + if(count!=0): + match_count+=1 + + overlap_ratio = match_count / len(source.points) *100 + return overlap_ratio \ No newline at end of file diff --git a/tools/plot_registration_plys.py b/tools/plot_registration_plys.py new file mode 100644 index 0000000000000000000000000000000000000000..d060d080d8375211fbf2b94e1bafedff1329a47c --- /dev/null +++ b/tools/plot_registration_plys.py @@ -0,0 +1,92 @@ +''' +Plot .ply files savedin results, etc. +Run on Google Colab or locally when registration is done on a headless server. +''' + +import sys +from pathlib import Path +from common.visualization import plot_point_cloud + +def _this_dir() -> Path: + # __file__ is not defined in notebooks / interactive sessions. + try: + return Path(__file__).resolve().parent # type: ignore[name-defined] + except NameError: + return Path.cwd() + + +def _ensure_imports() -> None: + # Allow running this file directly from the repo root. + repo_root = _this_dir() + if str(repo_root) not in sys.path: + sys.path.insert(0, str(repo_root)) + + +def _group_key_and_role(name: str) -> tuple[str | None, str | None]: + if name.endswith("_source_transformed.ply"): + return name[: -len("_source_transformed.ply")], "result" + if name.endswith("_source.ply"): + return name[: -len("_source.ply")], "source" + if name.endswith("_target.ply"): + return name[: -len("_target.ply")], "target" + return None, None + + +def main() -> int: + _ensure_imports() + + try: + import open3d as o3d # type: ignore + except Exception as e: + print(f"ERROR: open3d is required to read .ply files: {e}") + return 2 + + default_dir = _this_dir() / "results" / "registration_plys" + colab_drive_dir = Path("/content/drive/MyDrive/Colab Notebooks/registration_plys") + + # Super-simple CLI: optionally pass the directory as first non-flag argument + # that actually exists and is a directory. + # (In notebooks/IPython, argv often contains things like "-f ".) + dir_args = [] + for a in sys.argv[1:]: + if a.startswith("-"): + continue + p = Path(a).expanduser() + if p.exists() and p.is_dir(): + dir_args.append(p) + + if dir_args: + ply_dir = dir_args[0] + else: + ply_dir = colab_drive_dir if colab_drive_dir.exists() else default_dir + + ply_paths = sorted(ply_dir.glob("*.ply")) + if not ply_paths: + print(f"No .ply files found in: {ply_dir.resolve()}") + return 0 + + groups: dict[str, dict[str, Path]] = {} + for p in ply_paths[:6]: + key, role = _group_key_and_role(p.name) + if key is None or role is None: + continue + groups.setdefault(key, {})[role] = p + + for key in sorted(groups.keys()): + g = groups[key] + if not ("source" in g and "target" in g and "result" in g): + continue + + source = o3d.io.read_point_cloud(str(g["source"])) + target = o3d.io.read_point_cloud(str(g["target"])) + result = o3d.io.read_point_cloud(str(g["result"])) + + print(f"Plotting group: {key}") + plot_point_cloud(source, target, result=result) + + return 0 + + +if __name__ == "__main__": + main() + diff --git a/tools/predator_registration_and_evaluation.py b/tools/predator_registration_and_evaluation.py new file mode 100644 index 0000000000000000000000000000000000000000..17e7df395a5f0322a66c79e47223d24c9cf15776 --- /dev/null +++ b/tools/predator_registration_and_evaluation.py @@ -0,0 +1,386 @@ +import copy +import time +from dataclasses import dataclass +from pathlib import Path +from typing import Optional, Tuple + +import numpy as np +import open3d as o3d +import torch +try: + from easydict import EasyDict as edict # type: ignore +except Exception: # pragma: no cover + class edict(dict): + """Minimal EasyDict fallback (dot access).""" + + def __getattr__(self, k): + try: + return self[k] + except KeyError as e: + raise AttributeError(k) from e + + def __setattr__(self, k, v): + self[k] = v + +from tools import metrics +from r3pm_net.config_loader import get_method_paths + + +@dataclass +class _PredatorRunner: + predator_root: Path + config_path: Path + weights_path: Path + device: torch.device + config: edict + model: torch.nn.Module + neighborhood_limits: np.ndarray + input_num_points: int + + +_RUNNER: Optional[_PredatorRunner] = None +_METHOD_CFG = get_method_paths().get("predator", {}) + + +def _build_kpconv_architecture(num_layers: int) -> list: + # Mirrors the logic used in `master_thesis/OverlapPredator/scripts/demo.py`. + arch = ["simple", "resnetb"] + for _ in range(num_layers - 1): + arch += ["resnetb_strided", "resnetb", "resnetb"] + for _ in range(num_layers - 2): + arch += ["nearest_upsample", "unary"] + arch += ["nearest_upsample", "last_unary"] + return arch + + +def _get_predator_architecture(cfg_in: edict) -> list: + """ + OverlapPredator defines dataset-specific architectures in `configs/models.py`. + We try to use that (it must match the released checkpoints), and fall back to + the demo-style architecture builder if unavailable. + """ + try: + from configs.models import architectures as arch_dict # type: ignore + + dataset_name = getattr(cfg_in, "dataset", None) + if dataset_name in arch_dict: + return arch_dict[dataset_name] + except Exception: + pass + + return _build_kpconv_architecture(int(getattr(cfg_in, "num_layers", 3))) + + +def _resolve_path(predator_root: Path, p: str | Path) -> Path: + p = Path(p) + return p if p.is_absolute() else (predator_root / p) + + +def _maybe_downsample_xyz(xyz: np.ndarray, max_points: int) -> np.ndarray: + if max_points <= 0 or xyz.shape[0] <= max_points: + return xyz + idx = np.random.permutation(xyz.shape[0])[:max_points] + return xyz[idx] + + +def _to_o3d_feature(desc: np.ndarray) -> "o3d.pipelines.registration.Feature": + feat = o3d.pipelines.registration.Feature() + feat.data = np.asarray(desc, dtype=np.float32).T # (C, N) + return feat + + +def _ransac_pose_estimation( + src_xyz: np.ndarray, + tgt_xyz: np.ndarray, + src_desc: np.ndarray, + tgt_desc: np.ndarray, + *, + distance_threshold: float = 0.05, + ransac_n: int = 3, + mutual: bool = False, +) -> np.ndarray: + src_pcd = o3d.geometry.PointCloud(o3d.utility.Vector3dVector(src_xyz)) + tgt_pcd = o3d.geometry.PointCloud(o3d.utility.Vector3dVector(tgt_xyz)) + + src_feat = _to_o3d_feature(src_desc) + tgt_feat = _to_o3d_feature(tgt_desc) + + estimation = o3d.pipelines.registration.TransformationEstimationPointToPoint(False) + checkers = [ + o3d.pipelines.registration.CorrespondenceCheckerBasedOnEdgeLength(0.9), + o3d.pipelines.registration.CorrespondenceCheckerBasedOnDistance(distance_threshold), + ] + criteria = o3d.pipelines.registration.RANSACConvergenceCriteria(50000, 1000) + + # Open3D signature varies slightly by version; support both. + try: + result = o3d.pipelines.registration.registration_ransac_based_on_feature_matching( + src_pcd, + tgt_pcd, + src_feat, + tgt_feat, + mutual, + distance_threshold, + estimation, + ransac_n, + checkers, + criteria, + ) + except TypeError: + result = o3d.pipelines.registration.registration_ransac_based_on_feature_matching( + source=src_pcd, + target=tgt_pcd, + source_feature=src_feat, + target_feature=tgt_feat, + mutual_filter=mutual, + max_correspondence_distance=distance_threshold, + estimation_method=estimation, + ransac_n=ransac_n, + checkers=checkers, + criteria=criteria, + ) + + return np.asarray(result.transformation, dtype=np.float64) + + +def _init_runner( + predator_root: Path, + config_path: Path, + weights_path: Optional[Path], + *, + device: Optional[str | torch.device] = None, + input_num_points: Optional[int] = None, + calibrate_neighborhood_limits: bool = True, +) -> _PredatorRunner: + # Import OverlapPredator modules after adding it to sys.path. + import sys + + if str(predator_root) not in sys.path: + sys.path.insert(0, str(predator_root)) + + from lib.utils import load_config + from datasets.my_dataloader import calibrate_neighbors, collate_fn_descriptor + from models.architectures import KPFCNN + + cfg = edict(load_config(str(config_path))) + + if device is None: + device_t = torch.device("cuda" if bool(cfg.gpu_mode) and torch.cuda.is_available() else "cpu") + else: + device_t = torch.device(device) if not isinstance(device, torch.device) else device + + # Resolve weights path: + ckpt_path = _resolve_path(predator_root, weights_path) if weights_path else _resolve_path(predator_root, cfg.pretrain) + state = torch.load(str(ckpt_path), map_location=device_t) + state_dict = state["state_dict"] if isinstance(state, dict) and "state_dict" in state else state + + def _try_build_and_load(cfg_in: edict) -> Optional[torch.nn.Module]: + cfg_in.device = device_t + cfg_in.architecture = _get_predator_architecture(cfg_in) + m = KPFCNN(cfg_in).to(device_t) + m.eval() + try: + m.load_state_dict(state_dict, strict=False) + except RuntimeError: + return None + return m + + # First try the config as-is. If it fails (size mismatch), try common reduced widths. + cfg_candidates: list[edict] = [] + cfg_candidates.append(cfg) + + # Avoid duplicates while exploring smaller widths. + first_fd = int(getattr(cfg, "first_feats_dim", 0) or 0) + for cand in [first_fd // 2, 256, 128, 64]: + if cand and cand != first_fd: + c = edict(dict(cfg)) + c.first_feats_dim = int(cand) + cfg_candidates.append(c) + + model = None + chosen_cfg = None + for c in cfg_candidates: + m = _try_build_and_load(c) + if m is not None: + model = m + chosen_cfg = c + break + + if model is None or chosen_cfg is None: + # Re-raise with a clear message. + raise RuntimeError( + f"Failed to load OverlapPredator weights at '{ckpt_path}'. " + f"Config '{config_path}' seems incompatible with checkpoint tensor shapes." + ) + + # Decide input sampling count (ModelNet config uses 1024). + if input_num_points is None: + input_num_points = int(getattr(cfg, "num_points", 1024)) + + if calibrate_neighborhood_limits: + # Calibrate neighbors once using a minimal one-sample dataset. + class _SinglePairDataset: + def __init__(self, config): + self.config = config + + def __len__(self): + return 1 + + def __getitem__(self, _): + # Minimal valid sample to satisfy collate_fn_descriptor. + n = max(64, int(input_num_points)) + src = np.random.randn(n, 3).astype(np.float32) + tgt = np.random.randn(n, 3).astype(np.float32) + src_feats = np.ones((n, 1), dtype=np.float32) + tgt_feats = np.ones((n, 1), dtype=np.float32) + rot = np.eye(3, dtype=np.float32) + trans = np.zeros((3, 1), dtype=np.float32) + matching_inds = torch.ones(1, 2).long() + sample = torch.ones(1) + gt = np.eye(4, dtype=np.float32) + return src, tgt, src_feats, tgt_feats, rot, trans, matching_inds, src, tgt, sample, gt + + dummy_ds = _SinglePairDataset(chosen_cfg) + neighborhood_limits = calibrate_neighbors(dummy_ds, chosen_cfg, collate_fn=collate_fn_descriptor) + else: + # For tasks like parameter counting, we don't need KPConv neighborhood calibration. + # Pick a conservative default that works for typical KPConv configs. + n_layers = int(getattr(chosen_cfg, "num_layers", 5) or 5) + neighborhood_limits = np.asarray([256] * n_layers, dtype=np.int32) + + return _PredatorRunner( + predator_root=predator_root, + config_path=config_path, + weights_path=ckpt_path, + device=device_t, + config=chosen_cfg, + model=model, + neighborhood_limits=neighborhood_limits, + input_num_points=int(input_num_points), + ) + + +def predator_reg_and_eval( + source: "o3d.geometry.PointCloud", + target: "o3d.geometry.PointCloud", + *, + gt_transformation: Optional[np.ndarray] = None, + predator_root: str | Path = _METHOD_CFG.get("root", "/home/ykashefbahrami/master_thesis/OverlapPredator"), + config_path: str | Path = _METHOD_CFG.get("config_path", "/home/ykashefbahrami/master_thesis/OverlapPredator/configs/test/modelnet.yaml"), + weights_path: Optional[str | Path] = _METHOD_CFG.get("weights_path", None), + ransac_n_points: int = 1000, + ransac_distance_threshold: float = 0.05, + ransac_n: int = 3, + sampling: str = "prob", + mutual: bool = False, + device: Optional[str | torch.device] = None, + input_num_points: Optional[int] = 1024, +) -> Tuple["o3d.geometry.PointCloud", tuple]: + """ + Run OverlapPredator on a (source, target) pair and evaluate with the same + metric outputs as the Learning3D harness in this repo. + """ + global _RUNNER + predator_root_p = Path(predator_root).resolve() + config_path_p = Path(config_path).resolve() + weights_path_p = Path(weights_path).resolve() if weights_path is not None else None + + if _RUNNER is None: + _RUNNER = _init_runner( + predator_root_p, + config_path_p, + weights_path_p, + device=device, + input_num_points=input_num_points, + ) + + # Import OverlapPredator collate after sys.path is set by _init_runner. + from datasets.my_dataloader import collate_fn_descriptor + + src_xyz = np.asarray(source.points, dtype=np.float32) + tgt_xyz = np.asarray(target.points, dtype=np.float32) + src_xyz = _maybe_downsample_xyz(src_xyz, _RUNNER.input_num_points) + tgt_xyz = _maybe_downsample_xyz(tgt_xyz, _RUNNER.input_num_points) + + src_feats = np.ones((src_xyz.shape[0], 1), dtype=np.float32) + tgt_feats = np.ones((tgt_xyz.shape[0], 1), dtype=np.float32) + + rot = np.eye(3, dtype=np.float32) + trans = np.zeros((3, 1), dtype=np.float32) + matching_inds = torch.ones(1, 2).long() + sample = torch.ones(1) + gt = np.asarray(gt_transformation, dtype=np.float32) if gt_transformation is not None else np.eye(4, dtype=np.float32) + + # Collate into KPConv batch format. + batch = collate_fn_descriptor( + [(src_xyz, tgt_xyz, src_feats, tgt_feats, rot, trans, matching_inds, src_xyz, tgt_xyz, sample, gt)], + config=_RUNNER.config, + neighborhood_limits=_RUNNER.neighborhood_limits, + ) + + # Move batch tensors to device. + for k, v in list(batch.items()): + if isinstance(v, list): + batch[k] = [t.to(_RUNNER.device) for t in v] + elif torch.is_tensor(v): + batch[k] = v.to(_RUNNER.device) + + start = time.time() + with torch.no_grad(): + feats, scores_overlap, scores_saliency = _RUNNER.model(batch) + feats = feats.detach().cpu() + scores_overlap = scores_overlap.detach().cpu() + scores_saliency = scores_saliency.detach().cpu() + + pcd = batch["points"][0].detach().cpu() + len_src = int(batch["stack_lengths"][0][0].detach().cpu().item()) + src_pcd = pcd[:len_src] + tgt_pcd = pcd[len_src:] + + src_desc = feats[:len_src].numpy() + tgt_desc = feats[len_src:].numpy() + src_scores = (scores_overlap[:len_src] * scores_saliency[:len_src]).numpy().flatten() + tgt_scores = (scores_overlap[len_src:] * scores_saliency[len_src:]).numpy().flatten() + + def _sample_idx(scores: np.ndarray, n: int) -> np.ndarray: + n_all = scores.shape[0] + if n_all <= n: + return np.arange(n_all) + if sampling == "topk": + return np.argsort(-scores)[:n] + if sampling == "random": + return np.random.permutation(n_all)[:n] + # prob + s = float(scores.sum()) + if not np.isfinite(s) or s <= 0.0: + return np.random.permutation(n_all)[:n] + probs = scores / s + return np.random.choice(np.arange(n_all), size=n, replace=False, p=probs) + + src_idx = _sample_idx(src_scores, ransac_n_points) + tgt_idx = _sample_idx(tgt_scores, ransac_n_points) + + tsfm = _ransac_pose_estimation( + src_pcd[src_idx].numpy(), + tgt_pcd[tgt_idx].numpy(), + src_desc[src_idx], + tgt_desc[tgt_idx], + distance_threshold=ransac_distance_threshold, + ransac_n=ransac_n, + mutual=mutual, + ) + end = time.time() + + pc_result = copy.deepcopy(source).transform(tsfm) + eval_results = metrics.all_evaluations( + source, + target, + pc_result, + end - start, + gt_transformation=gt_transformation, + est_transformation=tsfm, + corres=None, + ) + + return pc_result, eval_results + diff --git a/tools/print_results.py b/tools/print_results.py new file mode 100644 index 0000000000000000000000000000000000000000..2f9d7981827b0103a4adc949731e681ca7ebda8b --- /dev/null +++ b/tools/print_results.py @@ -0,0 +1,45 @@ +import numpy as np +from tabulate import tabulate + +def print_table(reports): + # Prepare data for the table + table = [] + for method, report in reports.items(): + row = [ + method, + # report['mean_rmse'], + report['mean_rotation_error'], + report['mean_translation_error'], + report['mean_cd'], + # report['mean_error'], + report['mean_fitness'], + report['mean_inlier_rmse'], + report['mean_computation_time'] + ] + table.append(row) + + # headers for the table + # NOTE: report['mean_fitness'] is used as registration recall (success rate). + # headers = ['Method', 'RMSE', 'RE', 'TE', 'Time', 'CD', 'Res. Err.', 'Reg. Recall', 'Inlier RMSE'] + headers = ['Method', 'RRE', 'RTE', 'CD', 'Fitness', 'Inlier RMSE', 'Time'] + + print(tabulate(table, headers=headers, tablefmt='grid')) + +def print_table_no_gt_info(reports): + # Prepare data for the table + table = [] + for method, report in reports.items(): + row = [ + method, + report['mean_cd'], + report['mean_fitness'], + report['mean_inlier_rmse'], + report['mean_computation_time'] + ] + table.append(row) + + # headers for the table + # NOTE: report['mean_fitness'] is used as registration recall (success rate). + headers = ['Method','CD', 'Fitness', 'Inlier RMSE', 'Time'] + + print(tabulate(table, headers=headers, tablefmt='grid')) \ No newline at end of file diff --git a/tools/regtr_registration_and_evaluation.py b/tools/regtr_registration_and_evaluation.py new file mode 100644 index 0000000000000000000000000000000000000000..a86ef63c476de9540c123858f4d28623846ae27d --- /dev/null +++ b/tools/regtr_registration_and_evaluation.py @@ -0,0 +1,265 @@ +import copy +import sys +import time +from dataclasses import dataclass +from pathlib import Path +from typing import Optional, Tuple + +import numpy as np +import open3d as o3d +import torch + +from tools import metrics +from r3pm_net.config_loader import get_method_paths + + +try: + from easydict import EasyDict as edict # type: ignore +except Exception: # pragma: no cover + class edict(dict): + """Minimal EasyDict fallback (dot access).""" + + def __getattr__(self, k): + try: + return self[k] + except KeyError as e: + raise AttributeError(k) from e + + def __setattr__(self, k, v): + self[k] = v + + +@dataclass +class _RegTRRunner: + regtr_root: Path + regtr_src: Path + ckpt_path: Path + config_path: Path + device: torch.device + cfg: edict + model: torch.nn.Module + num_points: int + + +_RUNNER: Optional[_RegTRRunner] = None +_METHOD_CFG = get_method_paths().get("regtr", {}) + + +class _RegTRImportContext: + """Temporarily make RegTR's `src/` importable without polluting global imports. + + RegTR uses top-level packages like `models` and `utils`, which can collide with + other third-party repos loaded into the same Python process (e.g. OverlapPredator). + We therefore: + - temporarily add RegTR `src/` to sys.path + - import the needed symbols + - then restore sys.path and restore common conflicting sys.modules entries + """ + + _CONFLICT_PREFIXES = ( + "models", + "utils", + "cvhelpers", + "data_loaders", + "datasets", + "kernels", + ) + + def __init__(self, regtr_src: Path): + self.regtr_src = regtr_src + self._inserted = False + self._prev_modules: dict[str, object] = {} + self._cleared_keys: set[str] = set() + + def _iter_conflicting_module_keys(self) -> list[str]: + keys: list[str] = [] + for prefix in self._CONFLICT_PREFIXES: + if prefix in sys.modules: + keys.append(prefix) + dot = prefix + "." + for k in list(sys.modules.keys()): + if k.startswith(dot): + keys.append(k) + # de-dup while preserving order + seen = set() + out = [] + for k in keys: + if k not in seen: + seen.add(k) + out.append(k) + return out + + def __enter__(self): + if str(self.regtr_src) not in sys.path: + sys.path.insert(0, str(self.regtr_src)) + self._inserted = True + + # Save & clear potentially-colliding modules so `import models...` resolves + # to RegTR's `src/models`, not some other repo's `models` package. + for k in self._iter_conflicting_module_keys(): + if k in sys.modules: + self._prev_modules[k] = sys.modules[k] + sys.modules.pop(k, None) + self._cleared_keys.add(k) + return self + + def __exit__(self, exc_type, exc, tb): + # First remove any RegTR-introduced modules under the same prefixes, then restore. + for prefix in self._CONFLICT_PREFIXES: + sys.modules.pop(prefix, None) + dot = prefix + "." + for k in list(sys.modules.keys()): + if k.startswith(dot): + sys.modules.pop(k, None) + + for k, mod in self._prev_modules.items(): + sys.modules[k] = mod + + # Remove RegTR src path if we inserted it. + if self._inserted: + try: + sys.path.remove(str(self.regtr_src)) + except ValueError: + pass + return False + + +def _maybe_downsample_xyz(xyz: np.ndarray, max_points: int) -> np.ndarray: + if max_points <= 0 or xyz.shape[0] <= max_points: + return xyz + idx = np.random.permutation(xyz.shape[0])[:max_points] + return xyz[idx] + + +def _init_runner( + regtr_root: Path, + ckpt_path: Path, + config_path: Path, + *, + device: Optional[str | torch.device] = None, +) -> _RegTRRunner: + regtr_src = (regtr_root / "src").resolve() + if not regtr_src.exists(): + raise FileNotFoundError(f"RegTR src directory not found: {regtr_src}") + + if device is None: + device_t = torch.device("cuda" if torch.cuda.is_available() else "cpu") + else: + device_t = device if isinstance(device, torch.device) else torch.device(device) + + with _RegTRImportContext(regtr_src): + from utils.misc import load_config # type: ignore + from models.regtr import RegTR # type: ignore + + cfg = edict(load_config(str(config_path))) + model = RegTR(cfg).to(device_t) + + state = torch.load(str(ckpt_path), map_location=device_t) + state_dict = state["state_dict"] if isinstance(state, dict) and "state_dict" in state else state + model.load_state_dict(state_dict, strict=False) + model.eval() + + num_points = int(getattr(cfg, "num_points", 1024) or 1024) + return _RegTRRunner( + regtr_root=regtr_root, + regtr_src=regtr_src, + ckpt_path=ckpt_path, + config_path=config_path, + device=device_t, + cfg=cfg, + model=model, + num_points=num_points, + ) + + +def regtr_reg_and_eval( + source: "o3d.geometry.PointCloud", + target: "o3d.geometry.PointCloud", + *, + gt_transformation: Optional[np.ndarray] = None, + regtr_root: str | Path = _METHOD_CFG.get("root", "/home/ykashefbahrami/RegTR"), + ckpt_path: str | Path = _METHOD_CFG.get("ckpt_path", "/home/ykashefbahrami/RegTR/trained_models/modelnet/ckpt/model-best.pth"), + config_path: str | Path = _METHOD_CFG.get("config_path", "/home/ykashefbahrami/RegTR/trained_models/modelnet/config.yaml"), + device: Optional[str | torch.device] = None, +) -> Tuple["o3d.geometry.PointCloud", tuple]: + """Run RegTR (ModelNet checkpoint) on a (source, target) pair and evaluate. + + Returns: + pc_result: transformed copy of `source` (using estimated pose src->tgt) + eval_results: tuple shaped like `metrics.all_evaluations(...)` with GT provided + """ + global _RUNNER + + regtr_root_p = Path(regtr_root).resolve() + ckpt_path_p = Path(ckpt_path).resolve() + config_path_p = Path(config_path).resolve() + + if not ckpt_path_p.exists(): + raise FileNotFoundError( + f"RegTR checkpoint not found: {ckpt_path_p}\n" + f"Expected ModelNet weights at: {regtr_root_p}/trained_models/modelnet/ckpt/model-best.pth" + ) + if not config_path_p.exists(): + raise FileNotFoundError( + f"RegTR config not found: {config_path_p}\n" + f"Expected ModelNet config at: {regtr_root_p}/trained_models/modelnet/config.yaml" + ) + + if device is None: + requested_device = None + else: + requested_device = device if isinstance(device, torch.device) else torch.device(device) + + if ( + _RUNNER is None + or _RUNNER.regtr_root != regtr_root_p + or _RUNNER.ckpt_path != ckpt_path_p + or _RUNNER.config_path != config_path_p + or (requested_device is not None and _RUNNER.device != requested_device) + ): + _RUNNER = _init_runner(regtr_root_p, ckpt_path_p, config_path_p, device=device) + + src_xyz = np.asarray(source.points, dtype=np.float32) + tgt_xyz = np.asarray(target.points, dtype=np.float32) + src_xyz = _maybe_downsample_xyz(src_xyz, _RUNNER.num_points) + tgt_xyz = _maybe_downsample_xyz(tgt_xyz, _RUNNER.num_points) + + # Build batch the way RegTR expects it: list-of-tensors per batch element. + data_batch = { + "src_xyz": [torch.from_numpy(src_xyz).float().to(_RUNNER.device)], + "tgt_xyz": [torch.from_numpy(tgt_xyz).float().to(_RUNNER.device)], + } + + # Ensure RegTR's internal imports won't be confused by other repos. + # The forward path itself does not re-import, but its modules reference top-level + # packages (`models`, `utils`) which we keep isolated during the call. + with _RegTRImportContext(_RUNNER.regtr_src): + with torch.no_grad(): + # Warm-up to avoid first-run overhead in timings. + _RUNNER.model(data_batch) + + start = time.time() + with torch.no_grad(): + outputs = _RUNNER.model(data_batch) + end = time.time() + + pose = outputs["pose"][-1, 0].detach().cpu().numpy() + if pose.shape != (4, 4): + # pad a row of [0, 0, 0, 1] to the pose because the pose is a 3x4 matrix in the original code + pose = np.vstack([pose, [0, 0, 0, 1]]) + if pose.shape != (4, 4): # sanity check, should not happen + raise ValueError(f"Unexpected RegTR pose shape: {pose.shape}") + pose = pose.astype(np.float64) + + pc_result = copy.deepcopy(source).transform(pose) + eval_results = metrics.all_evaluations( + source, + target, + pc_result, + end - start, + gt_transformation=gt_transformation, + est_transformation=pose, + corres=None, + ) + return pc_result, eval_results + diff --git a/tools/transformations.py b/tools/transformations.py new file mode 100644 index 0000000000000000000000000000000000000000..bb966dc703f7fb2905db88600d370079688a4a89 --- /dev/null +++ b/tools/transformations.py @@ -0,0 +1,39 @@ +import numpy as np +import open3d as o3d + +def validate_rotation(rotation): + # Check if the rotation matrix is valid (orthogonal and determinant = 1) + is_orthogonal = np.allclose(np.dot(rotation[:3, :3], rotation[:3, :3].T), np.eye(3)) + is_determinant_one = np.isclose(np.linalg.det(rotation[:3, :3]), 1) + return is_orthogonal and is_determinant_one + +def random_translation(translation_range): + return np.random.uniform(translation_range[0], translation_range[1], size=3) + + +def create_transformation(x_angle, y_angle, z_angle, translation_range): + ''' + Generate a random transformation matrix with given rotation angles and translation range using open3d methods. + + Args: + x_angle (float): Rotation angle around the x-axis in degrees. + y_angle (float): Rotation angle around the y-axis in degrees. + z_angle (float): Rotation angle around the z-axis in degrees. + translation_range (list): Range for random translation [min, max]. + + Returns: + np.ndarray: A 4x4 transformation matrix. + ''' + x_angle = np.deg2rad(x_angle) + y_angle = np.deg2rad(y_angle) + z_angle = np.deg2rad(z_angle) + + rotation = o3d.geometry.get_rotation_matrix_from_zxy([z_angle, y_angle, x_angle]) + translation = random_translation(translation_range) + + # Create a 4x4 transformation matrix + random_transformation = np.eye(4) + random_transformation[:3, :3] = rotation + random_transformation[:3, 3] = translation + + return random_transformation \ No newline at end of file diff --git a/tools/visualization.py b/tools/visualization.py new file mode 100644 index 0000000000000000000000000000000000000000..4c83c386d1e63472ac2c813241d717da21638d47 --- /dev/null +++ b/tools/visualization.py @@ -0,0 +1,204 @@ +import numpy as np +import open3d as o3d +import plotly.graph_objects as go +import copy +import os +from pathlib import Path +from datetime import datetime + +def get_color(deg): + ''' + This function is used to determine the color of the arrow that shows angle error. + It gets color based on the degree of rotation. + ''' + deg = abs(deg) + if deg < 5: + return 'green' + elif deg < 10: + return 'orange' + else: + return 'red' + +def plot_point_cloud(source, target, result = None, show_grid=False, x_diff=None, y_diff=None, z_diff=None): + ''' + Visualizes two point clouds and optionally a result point cloud. + + args: + source: o3d.geometry.PointCloud, the source point cloud to visualize + target: o3d.geometry.PointCloud, the target point cloud to visualize + result: o3d.geometry.PointCloud, optional, the result point cloud to visualize + show_grid: bool, optional, whether to show the grid in the 3D plot + x_diff: float, optional, the difference in X angle to visualize + y_diff: float, optional, the difference in Y angle to visualize + z_diff: float, optional, the difference in Z angle to visualize + + result: + A 3D plotly figure visualizing the source and target point clouds, and optionally + the result point cloud, with arrows indicating angle differences. + ''' + # Point cloud data + source_points = np.asarray(source.points) + target_points = np.asarray(target.points) + + if result is not None: + result_points = np.asarray(result.points) + + source_scatter = go.Scatter3d( + x=source_points[:, 0], y=source_points[:, 1], z=source_points[:, 2], + mode='markers', marker=dict(size=2, color='rgb(255, 180, 0)'), name='Source (Yellow)') + + target_scatter = go.Scatter3d( + x=target_points[:, 0], y=target_points[:, 1], z=target_points[:, 2], + mode='markers', marker=dict(size=2, color='rgb(0, 166, 237)'), name='Target (Blue)') + + data = [source_scatter, target_scatter] + + if result is not None: + result_scatter = go.Scatter3d( + x=result_points[:, 0], y=result_points[:, 1], z=result_points[:, 2], + # mode='markers', marker=dict(size=2, color='rgb(153, 0, 76)'), name='Result (Purple)') + mode='markers', marker=dict(size=2, color='rgb(255, 180, 0)'), name='Result (Yellow)') + data.append(result_scatter) + + # Create the figure + fig = go.Figure(data=data) + + # Plots arrows to show angle difference + if x_diff and y_diff and z_diff: + all_points = [source_points, target_points] + if result is not None: + all_points.append(result_points) + + center = np.mean(np.vstack(all_points), axis=0) + scale_factor = 0.01 # Adjust this for visual scaling + + angles = [x_diff, y_diff, z_diff] + axes = [ + {"axis": "X", "vec": np.array([1, 0, 0]), "angle": angles[0], "label": f'ΔX (Roll): {angles[0]:.2f}°', "color": get_color(angles[0])}, + {"axis": "Y", "vec": np.array([0, 1, 0]), "angle": angles[1], "label": f'ΔY (Pitch): {angles[1]:.2f}°', "color": get_color(angles[1])}, + {"axis": "Z", "vec": np.array([0, 0, 1]), "angle": angles[2], "label": f'ΔZ (Yaw): {angles[2]:.2f}°', "color": get_color(angles[2])}, + ] + + for axis in axes: + magnitude = abs(axis["angle"]) * scale_factor + vec = axis["vec"] * axis["angle"] * scale_factor + tip = center + vec + color = axis["color"] + + # Line (arrow) + fig.add_trace(go.Scatter3d( + x=[center[0], tip[0]], y=[center[1], tip[1]], z=[center[2], tip[2]], + mode='lines', + line=dict(color=color, width=5), + showlegend=False + )) + + # Arrow Head (cone) + fig.add_trace(go.Cone( + x=[tip[0]], y=[tip[1]], z=[tip[2]], + u=[vec[0]], v=[vec[1]], w=[vec[2]], + colorscale=[[0, color], [1, color]], + showscale=False, + sizemode="absolute", + sizeref=0.04 * magnitude, + anchor="tip" + )) + + # Annotation + text_pos = tip + 0.02 * np.sign(vec) + fig.add_trace(go.Scatter3d( + x=[text_pos[0]], y=[text_pos[1]], z=[text_pos[2]], + mode='text', + text=[axis["label"]], + textposition="top center", + showlegend=False, + textfont=dict(size=14, color=color) + )) + + fig.update_layout( + scene=dict( + xaxis=dict(visible=show_grid), + yaxis=dict(visible=show_grid), + zaxis=dict(visible=show_grid), + aspectmode='data' + ), + width=900, + height=700 + ) + + fig.show() + +# Open3d helper function to draw 2 point clouds +def draw_point_clouds(pcd1, pcd2): + ''' + args: + pcd1: o3d.geometry.PointCloud + pcd2: o3d.geometry.PointCloud + + result: + Visualizes pcd2 with yellow and pcd1 with cyan ransformed with an alignment transformation. + ''' + pcd1_temp = copy.deepcopy(pcd1) + pcd2_temp = copy.deepcopy(pcd2) + pcd1_temp.paint_uniform_color([1, 0.706, 0]) + pcd2_temp.paint_uniform_color([0, 0.651, 0.929]) + o3d.visualization.draw_geometries([pcd1_temp, pcd2_temp], + zoom=0.4459, + front=[0.9288, -0.2951, -0.2242], + lookat=[1.6784, 2.0612, 1.4451], + up=[-0.3402, -0.9189, -0.1996]) + +# open3d helper function to draw registration result +def draw_registration_result(source, target, transformation, method_name = None): + ''' + args: + source: o3d.geometry.PointCloud + target: o3d.geometry.PointCloud + transformation: np.array, 4x4 matrix (an intial guess of the transformation to roughly align PCs on top of each other) --> Global registration + + result: + Saves source, target, and transformed source point clouds as .ply files (for later viewing). + If a display is available, also visualizes the registration result. + ''' + # Make copies so we never mutate the inputs + source_orig = copy.deepcopy(source) + target_temp = copy.deepcopy(target) + source_temp = copy.deepcopy(source) + + # Color for easier later inspection + # source_orig.paint_uniform_color([1, 0.706, 0]) + # source_temp.paint_uniform_color([1, 0.706, 0]) + # target_temp.paint_uniform_color([0, 0.651, 0.929]) + + # Apply transformation to the "result" copy + source_temp.transform(transformation) + + # Save point clouds for offline visualization + out_dir = Path(__file__).resolve().parents[1] / "results" / "registration_plys" + out_dir.mkdir(parents=True, exist_ok=True) + + stamp = datetime.now().strftime("%d_%H%M_%f") + source_path = out_dir / f"{stamp}_source.ply" + target_path = out_dir / f"{stamp}_target.ply" + transformed_path = out_dir / f"{stamp}_source_transformed.ply" + + try: + if method_name is not None: + source_path = out_dir / f"{stamp}_{method_name}_source.ply" + target_path = out_dir / f"{stamp}_{method_name}_target.ply" + transformed_path = out_dir / f"{stamp}_{method_name}_source_transformed.ply" + + o3d.io.write_point_cloud(str(source_path), source_orig) + o3d.io.write_point_cloud(str(target_path), target_temp) + o3d.io.write_point_cloud(str(transformed_path), source_temp) + print(f"[visualization] Saved .ply files to: {out_dir}") + except Exception as e: + print(f"[visualization] WARNING: Failed to write .ply files to {out_dir}: {e}") + + # Only try to open a window if a display exists (avoid headless GLFW warnings) + if os.environ.get("DISPLAY"): + o3d.visualization.draw_geometries([source_temp, target_temp], + zoom=0.4459, + front=[0.9288, -0.2951, -0.2242], + lookat=[1.6784, 2.0612, 1.4451], + up=[-0.3402, -0.9189, -0.1996]) \ No newline at end of file