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
+
+
+
+
+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