├── .gitignore
├── LICENSE
├── README.md
├── auxiliary
└── ChamferDistancePytorch
│ ├── chamfer3D
│ ├── .gitignore
│ ├── chamfer3D.cu
│ ├── chamfer_cuda.cpp
│ ├── dist_chamfer_3D.py
│ └── setup.py
│ ├── chamfer_python.py
│ ├── fscore.py
│ └── unit_test.py
├── compile_chamfer_distance_op.sh
├── datasets
├── KITTI_mapping.txt
├── flyingthings3d_flownet3d.py
├── flyingthings3d_hplflownet.py
├── generic.py
├── kitti_flownet3d.py
└── kitti_hplflownet.py
├── doc
├── poster.png
├── scoop_result.gif
└── slides.pdf
├── download_pretrained_models.sh
├── install_environment.sh
├── models
├── gconv.py
├── graph.py
├── refiner.py
└── scoop.py
├── scripts
├── evaluate_on_ft3d_s.sh
├── evaluate_on_kitti_o.sh
├── evaluate_on_kitti_o_all_points.sh
├── evaluate_on_kitti_s.sh
├── evaluate_on_kitti_t.sh
├── evaluate_on_kitti_t_all_points.sh
├── evaluate_scoop.py
├── train_on_ft3d_o.sh
├── train_on_ft3d_s.sh
├── train_on_kitti_v.sh
├── train_scoop.py
└── visualize_scoop.py
└── tools
├── losses.py
├── ot.py
├── reconstruction.py
├── seed.py
├── utils.py
└── vis_utils.py
/.gitignore:
--------------------------------------------------------------------------------
1 | data/
2 | experiments/
3 | pretrained_models/
4 | .idea/
5 | __pycache__/
6 | *.egg-info
7 | *.pyc
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | SCOOP: Self-Supervised Correspondence and Optimization-Based Scene Flow
2 |
3 | MIT License
4 |
5 | Copyright (c) 2022 Itai Lang
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SCOOP: Self-Supervised Correspondence and Optimization-Based Scene Flow
2 | [[Project Page]](https://itailang.github.io/SCOOP/) [[Paper]](https://openaccess.thecvf.com/content/CVPR2023/html/Lang_SCOOP_Self-Supervised_Correspondence_and_Optimization-Based_Scene_Flow_CVPR_2023_paper.html) [[Video]](https://www.youtube.com/watch?v=b8MVWGU7V4E) [[Slides]](./doc/slides.pdf) [[Poster]](./doc/poster.png)
3 |
4 | Created by [Itai Lang](https://itailang.github.io/)1,2, [Dror Aiger](https://research.google/people/DrorAiger/)2, [Forrester Cole](http://people.csail.mit.edu/fcole/)2, [Shai Avidan](http://www.eng.tau.ac.il/~avidan/)1, and [Michael Rubinstein](http://people.csail.mit.edu/mrub/)2.
5 | 1Tel Aviv University 2Google Research
6 |
7 | 
8 |
9 | ## Abstract
10 | Scene flow estimation is a long-standing problem in computer vision, where the goal is to find the 3D motion of a scene from its consecutive observations.
11 | Recently, there have been efforts to compute the scene flow from 3D point clouds.
12 | A common approach is to train a regression model that consumes source and target point clouds and outputs the per-point translation vector.
13 | An alternative is to learn point matches between the point clouds concurrently with regressing a refinement of the initial correspondence flow.
14 | In both cases, the learning task is very challenging since the flow regression is done in the free 3D space, and a typical solution is to resort to a large annotated synthetic dataset.
15 |
16 | We introduce SCOOP, a new method for scene flow estimation that can be learned on a small amount of data without employing ground-truth flow supervision.
17 | In contrast to previous work, we train a pure correspondence model focused on learning point feature representation and initialize the flow as the difference between a source point and its softly corresponding target point.
18 | Then, in the run-time phase, we directly optimize a flow refinement component with a self-supervised objective, which leads to a coherent and accurate flow field between the point clouds.
19 | Experiments on widespread datasets demonstrate the performance gains achieved by our method compared to existing leading techniques while using a fraction of the training data.
20 |
21 | ## Citation
22 | If you find our work useful in your research, please consider citing:
23 |
24 | @InProceedings{lang2023scoop,
25 | author = {Lang, Itai and Aiger, Dror and Cole, Forrester and Avidan, Shai and Rubinstein, Michael},
26 | title = {{SCOOP: Self-Supervised Correspondence and Optimization-Based Scene Flow}},
27 | booktitle = {Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR)},
28 | pages = {5281--5290},
29 | year = {2023}
30 | }
31 |
32 | ## Installation
33 | The code has been tested with Python 3.6.13, PyTorch 1.6.0, CUDA 10.1, and cuDNN 7.6.5 on Ubuntu 16.04.
34 |
35 | Clone this repository:
36 | ```bash
37 | git clone https://github.com/itailang/SCOOP.git
38 | cd SCOOP/
39 | ```
40 |
41 | Create a conda environment:
42 | ```bash
43 | # create and activate a conda environment
44 | conda create -n scoop python=3.6.13 --yes
45 | conda activate scoop
46 | ```
47 |
48 | Install required packages:
49 | ```bash
50 | sh install_environment.sh
51 | ```
52 |
53 | Compile the Chamfer Distance op, implemented by [Groueix _et al._](https://github.com/ThibaultGROUEIX/ChamferDistancePytorch) The op is located under `auxiliary/ChamferDistancePytorch/chamfer3D` folder. The following compilation script uses a CUDA 10.1 path. If needed, modify script to point to your CUDA path. Then, use:
54 | ```bash
55 | sh compile_chamfer_distance_op.sh
56 | ```
57 |
58 | The compilation results should be created under `auxiliary/ChamferDistancePytorch/chamfer3D/build` folder.
59 |
60 | ## Usage
61 |
62 | ### Data
63 | Create folders for the data:
64 | ```bash
65 | mkdir ./data/
66 | mkdir ./data/FlowNet3D/
67 | mkdir ./data/HPLFlowNet/
68 | ```
69 |
70 | We use the point cloud data version prepared Liu _et al._ from their work [FlowNet3D](https://openaccess.thecvf.com/content_CVPR_2019/html/Liu_FlowNet3D_Learning_Scene_Flow_in_3D_Point_Clouds_CVPR_2019_paper.html). Please follow their [code](https://github.com/xingyul/flownet3d) to acquire the data.
71 | * Put the preprocessed FlyingThings3D dataset at `./data/FlowNet3D/data_processed_maxcut_35_20k_2k_8192/`. This data is denoted as FT3Do.
72 | * Put the preprocessed KITTI dataset at `./data/flownet3d/kitti_rm_ground/`. This dataset is denoted as KITTIo and its subsets are denoted as KITTIv and KITTIt.
73 |
74 | We also use the point cloud data version prepared Gu _et al._ from their work [HPLFlowNet](https://openaccess.thecvf.com/content_CVPR_2019/html/Gu_HPLFlowNet_Hierarchical_Permutohedral_Lattice_FlowNet_for_Scene_Flow_Estimation_on_CVPR_2019_paper.html). Please follow their [code](https://github.com/laoreja/HPLFlowNet) to acquire the data.
75 | * Put the preprocessed FlyingThings3D dataset at `./data/HPLFlowNet/FlyingThings3D_subset_processed_35m/`. This dataset is denoted as FT3Ds.
76 | * Put the preprocessed KITTI dataset at `./data/flownet3d/KITTI_processed_occ_final/`. This dataset is denoted as KITTIs.
77 |
78 | Note that you may put the data elsewhere and create a symbolic link to the actual location. For example:
79 | ```bash
80 | ln -s /path/to/the/actual/dataset/location ./data/FlowNet3D/data_processed_maxcut_35_20k_2k_8192
81 | ```
82 |
83 | ### Training and Evaluation
84 | Switch to the `scripts` folder:
85 | ```bash
86 | cd ./scripts
87 | ```
88 |
89 | #### FT3Do / KITTIo
90 | To train a model on 1,800 examples from the train set of FT3Do, run the following command:
91 | ```bash
92 | sh train_on_ft3d_o.sh
93 | ```
94 |
95 | Evaluate this model on KITTIo with 2,048 point per point cloud using the following command:
96 | ```bash
97 | sh evaluate_on_kitti_o.sh
98 | ```
99 |
100 | The results will be saved to the file `./experiments/ft3d_o_1800_examples/log_evaluation_kitti_o.txt`.
101 |
102 | Evaluate this model on KITTIo with all the points in the point clouds using the following command:
103 | ```bash
104 | sh evaluate_on_kitti_o_all_point.sh
105 | ```
106 |
107 | The results will be saved to the file `./experiments/ft3d_o_1800_examples/log_evaluation_kitti_o_all_points.txt`.
108 |
109 | #### KITTIv / KITTIt
110 | To train a model on the 100 examples of KITTIv, run the following command:
111 | ```bash
112 | sh train_on_kitti_v.sh
113 | ```
114 |
115 | Evaluate this model on KITTIt with 2,048 point per point cloud using the following command:
116 | ```bash
117 | sh evaluate_on_kitti_t.sh
118 | ```
119 |
120 | The results will be saved to the file `./experiments/kitti_v_100_examples/log_evaluation_kitti_t.txt`.
121 |
122 | Evaluate this model on KITTIt with all the points in the point clouds using the following command:
123 | ```bash
124 | sh evaluate_on_kitti_t_all_points.sh
125 | ```
126 |
127 | The results will be saved to the file `./experiments/kitti_v_100_examples/log_evaluation_kitti_t_all_points.txt`.
128 |
129 | #### FT3Ds / KITTIs, FT3Ds / FT3Ds
130 | To train a model on 1,800 examples from the train set FT3Ds, run the following command:
131 | ```bash
132 | sh train_on_ft3d_s.sh
133 | ```
134 |
135 | Evaluate this model on KITTIs with 8,192 point per point cloud using the following command:
136 | ```bash
137 | sh evaluate_on_kitti_s.sh
138 | ```
139 |
140 | The results will be saved to the file `./experiments/ft3d_s_1800_examples/log_evaluation_kitti_s.txt`.
141 |
142 | Evaluate this model on the test set of FT3Ds with 8,192 point per point cloud using the following command:
143 | ```bash
144 | sh evaluate_on_ft3d_s.sh
145 | ```
146 |
147 | The results will be saved to the file `./experiments/ft3d_s_1800_examples/log_evaluation_ft3d_s.txt`.
148 |
149 | #### Visualization
150 | First, save results for visualization by adding the flag `--save_pc_res 1` when running the evaluation script. For example, the [script for evaluating on KITTIt](./scripts/evaluate_on_kitti_t.sh).
151 | The results will be saved to the folder `./experiments/kitti_v_100_examples/pc_res/`.
152 |
153 | Then, select the scene index that you would like to visualize and run the visualization script. For example, visualizing scene index #1 from KITTIt:
154 | ```python
155 | python visualize_scoop.py --res_dir ./../experiments/kitti_v_100_examples/pc_res --res_idx 1
156 | ```
157 |
158 | The visualizations will be saved to the folder `./experiments/kitti_v_100_examples/pc_res/vis/`.
159 |
160 | ### Evaluation with Pretrained Models
161 |
162 | First, download our pretrained models with the following command:
163 | ```bash
164 | bash download_pretrained_models.sh
165 | ```
166 |
167 | The models (about 2MB) will be saved under `pretrained_models` folder in the following structure:
168 | ```
169 | pretrained_models/
170 | ├── ft3d_o_1800_examples/model_e100.tar
171 | ├── ft3d_s_1800_examples/model_e060.tar
172 | ├── kitti_v_100_examples/model_e400.tar
173 | ```
174 |
175 | Then, use the evaluation commands mentioned in section [Training and Evaluation](#training-and-Evaluation),
176 | after changing the `experiments` folder in the evaluation scripts to the `pretrained_models` folder.
177 |
178 | ## License
179 | This project is licensed under the terms of the MIT license (see the [LICENSE](./LICENSE) file for more details).
180 |
181 | ## Acknowledgment
182 | Our code builds upon the code provided by [Puy _et al._](https://github.com/valeoai/FLOT), [Groueix _et al._](https://github.com/ThibaultGROUEIX/ChamferDistancePytorch), [Liu _et al._](https://github.com/xingyul/flownet3d), and [Gu _et al._](https://github.com/laoreja/HPLFlowNet) We thank the authors for sharing their code.
183 |
--------------------------------------------------------------------------------
/auxiliary/ChamferDistancePytorch/chamfer3D/.gitignore:
--------------------------------------------------------------------------------
1 | build/
2 | chamfer_3D.egg-info/
3 | dist/
4 |
--------------------------------------------------------------------------------
/auxiliary/ChamferDistancePytorch/chamfer3D/chamfer3D.cu:
--------------------------------------------------------------------------------
1 |
2 | #include
3 | #include
4 |
5 | #include
6 | #include
7 |
8 | #include
9 |
10 |
11 |
12 | __global__ void NmDistanceKernel(int b,int n,const float * xyz,int m,const float * xyz2,float * result,int * result_i){
13 | const int batch=512;
14 | __shared__ float buf[batch*3];
15 | for (int i=blockIdx.x;ibest){
127 | result[(i*n+j)]=best;
128 | result_i[(i*n+j)]=best_i;
129 | }
130 | }
131 | __syncthreads();
132 | }
133 | }
134 | }
135 | // int chamfer_cuda_forward(int b,int n,const float * xyz,int m,const float * xyz2,float * result,int * result_i,float * result2,int * result2_i, cudaStream_t stream){
136 | int chamfer_cuda_forward(at::Tensor xyz1, at::Tensor xyz2, at::Tensor dist1, at::Tensor dist2, at::Tensor idx1, at::Tensor idx2){
137 |
138 | const auto batch_size = xyz1.size(0);
139 | const auto n = xyz1.size(1); //num_points point cloud A
140 | const auto m = xyz2.size(1); //num_points point cloud B
141 |
142 | NmDistanceKernel<<>>(batch_size, n, xyz1.data(), m, xyz2.data(), dist1.data(), idx1.data());
143 | NmDistanceKernel<<>>(batch_size, m, xyz2.data(), n, xyz1.data(), dist2.data(), idx2.data());
144 |
145 | cudaError_t err = cudaGetLastError();
146 | if (err != cudaSuccess) {
147 | printf("error in nnd updateOutput: %s\n", cudaGetErrorString(err));
148 | //THError("aborting");
149 | return 0;
150 | }
151 | return 1;
152 |
153 |
154 | }
155 | __global__ void NmDistanceGradKernel(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){
156 | for (int i=blockIdx.x;i>>(batch_size,n,xyz1.data(),m,xyz2.data(),graddist1.data(),idx1.data(),gradxyz1.data(),gradxyz2.data());
185 | NmDistanceGradKernel<<>>(batch_size,m,xyz2.data(),n,xyz1.data(),graddist2.data(),idx2.data(),gradxyz2.data(),gradxyz1.data());
186 |
187 | cudaError_t err = cudaGetLastError();
188 | if (err != cudaSuccess) {
189 | printf("error in nnd get grad: %s\n", cudaGetErrorString(err));
190 | //THError("aborting");
191 | return 0;
192 | }
193 | return 1;
194 |
195 | }
196 |
197 |
--------------------------------------------------------------------------------
/auxiliary/ChamferDistancePytorch/chamfer3D/chamfer_cuda.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 |
4 | ///TMP
5 | //#include "common.h"
6 | /// NOT TMP
7 |
8 |
9 | int chamfer_cuda_forward(at::Tensor xyz1, at::Tensor xyz2, at::Tensor dist1, at::Tensor dist2, at::Tensor idx1, at::Tensor idx2);
10 |
11 |
12 | int chamfer_cuda_backward(at::Tensor xyz1, at::Tensor xyz2, at::Tensor gradxyz1, at::Tensor gradxyz2, at::Tensor graddist1, at::Tensor graddist2, at::Tensor idx1, at::Tensor idx2);
13 |
14 |
15 |
16 |
17 | int chamfer_forward(at::Tensor xyz1, at::Tensor xyz2, at::Tensor dist1, at::Tensor dist2, at::Tensor idx1, at::Tensor idx2) {
18 | return chamfer_cuda_forward(xyz1, xyz2, dist1, dist2, idx1, idx2);
19 | }
20 |
21 |
22 | int chamfer_backward(at::Tensor xyz1, at::Tensor xyz2, at::Tensor gradxyz1, at::Tensor gradxyz2, at::Tensor graddist1,
23 | at::Tensor graddist2, at::Tensor idx1, at::Tensor idx2) {
24 |
25 | return chamfer_cuda_backward(xyz1, xyz2, gradxyz1, gradxyz2, graddist1, graddist2, idx1, idx2);
26 | }
27 |
28 |
29 |
30 | PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
31 | m.def("forward", &chamfer_forward, "chamfer forward (CUDA)");
32 | m.def("backward", &chamfer_backward, "chamfer backward (CUDA)");
33 | }
--------------------------------------------------------------------------------
/auxiliary/ChamferDistancePytorch/chamfer3D/dist_chamfer_3D.py:
--------------------------------------------------------------------------------
1 | from torch import nn
2 | from torch.autograd import Function
3 | import torch
4 | import importlib
5 | import os
6 | chamfer_found = importlib.find_loader("chamfer_3D") is not None
7 | if not chamfer_found:
8 | ## Cool trick from https://github.com/chrdiller
9 | print("Jitting Chamfer 3D")
10 |
11 | from torch.utils.cpp_extension import load
12 | chamfer_3D = load(name="chamfer_3D",
13 | sources=[
14 | "/".join(os.path.abspath(__file__).split('/')[:-1] + ["chamfer_cuda.cpp"]),
15 | "/".join(os.path.abspath(__file__).split('/')[:-1] + ["chamfer3D.cu"]),
16 | ])
17 | print("Loaded JIT 3D CUDA chamfer distance")
18 |
19 | else:
20 | import chamfer_3D
21 | print("Loaded compiled 3D CUDA chamfer distance")
22 |
23 |
24 | # Chamfer's distance module @thibaultgroueix
25 | # GPU tensors only
26 | class chamfer_3DFunction(Function):
27 | @staticmethod
28 | def forward(ctx, xyz1, xyz2):
29 | batchsize, n, _ = xyz1.size()
30 | _, m, _ = xyz2.size()
31 | device = xyz1.device
32 |
33 | dist1 = torch.zeros(batchsize, n)
34 | dist2 = torch.zeros(batchsize, m)
35 |
36 | idx1 = torch.zeros(batchsize, n).type(torch.IntTensor)
37 | idx2 = torch.zeros(batchsize, m).type(torch.IntTensor)
38 |
39 | dist1 = dist1.to(device)
40 | dist2 = dist2.to(device)
41 | idx1 = idx1.to(device)
42 | idx2 = idx2.to(device)
43 | torch.cuda.set_device(device)
44 |
45 | chamfer_3D.forward(xyz1, xyz2, dist1, dist2, idx1, idx2)
46 | ctx.save_for_backward(xyz1, xyz2, idx1, idx2)
47 | return dist1, dist2, idx1, idx2
48 |
49 | @staticmethod
50 | def backward(ctx, graddist1, graddist2, gradidx1, gradidx2):
51 | xyz1, xyz2, idx1, idx2 = ctx.saved_tensors
52 | graddist1 = graddist1.contiguous()
53 | graddist2 = graddist2.contiguous()
54 | device = graddist1.device
55 |
56 | gradxyz1 = torch.zeros(xyz1.size())
57 | gradxyz2 = torch.zeros(xyz2.size())
58 |
59 | gradxyz1 = gradxyz1.to(device)
60 | gradxyz2 = gradxyz2.to(device)
61 | chamfer_3D.backward(
62 | xyz1, xyz2, gradxyz1, gradxyz2, graddist1, graddist2, idx1, idx2
63 | )
64 | return gradxyz1, gradxyz2
65 |
66 |
67 | class chamfer_3DDist(nn.Module):
68 | def __init__(self):
69 | super(chamfer_3DDist, self).__init__()
70 |
71 | def forward(self, input1, input2):
72 | input1 = input1.contiguous()
73 | input2 = input2.contiguous()
74 | return chamfer_3DFunction.apply(input1, input2)
75 |
76 |
--------------------------------------------------------------------------------
/auxiliary/ChamferDistancePytorch/chamfer3D/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 | from torch.utils.cpp_extension import BuildExtension, CUDAExtension
3 |
4 | setup(
5 | name='chamfer_3D',
6 | ext_modules=[
7 | CUDAExtension('chamfer_3D', [
8 | "/".join(__file__.split('/')[:-1] + ['chamfer_cuda.cpp']),
9 | "/".join(__file__.split('/')[:-1] + ['chamfer3D.cu']),
10 | ]),
11 | ],
12 | cmdclass={
13 | 'build_ext': BuildExtension
14 | })
15 |
--------------------------------------------------------------------------------
/auxiliary/ChamferDistancePytorch/chamfer_python.py:
--------------------------------------------------------------------------------
1 | import torch
2 |
3 |
4 | def pairwise_dist(x, y):
5 | xx, yy, zz = torch.mm(x, x.t()), torch.mm(y, y.t()), torch.mm(x, y.t())
6 | rx = xx.diag().unsqueeze(0).expand_as(xx)
7 | ry = yy.diag().unsqueeze(0).expand_as(yy)
8 | P = rx.t() + ry - 2 * zz
9 | return P
10 |
11 |
12 | def NN_loss(x, y, dim=0):
13 | dist = pairwise_dist(x, y)
14 | values, indices = dist.min(dim=dim)
15 | return values.mean()
16 |
17 |
18 | def distChamfer(a, b):
19 | """
20 | :param a: Pointclouds Batch x nul_points x dim
21 | :param b: Pointclouds Batch x nul_points x dim
22 | :return:
23 | -closest point on b of points from a
24 | -closest point on a of points from b
25 | -idx of closest point on b of points from a
26 | -idx of closest point on a of points from b
27 | Works for pointcloud of any dimension
28 | """
29 | x, y = a.double(), b.double()
30 | bs, num_points_x, points_dim = x.size()
31 | bs, num_points_y, points_dim = y.size()
32 |
33 | xx = torch.pow(x, 2).sum(2)
34 | yy = torch.pow(y, 2).sum(2)
35 | zz = torch.bmm(x, y.transpose(2, 1))
36 | rx = xx.unsqueeze(1).expand(bs, num_points_y, num_points_x) # Diagonal elements xx
37 | ry = yy.unsqueeze(1).expand(bs, num_points_x, num_points_y) # Diagonal elements yy
38 | P = rx.transpose(2, 1) + ry - 2 * zz
39 | return torch.min(P, 2)[0].float(), torch.min(P, 1)[0].float(), torch.min(P, 2)[1].int(), torch.min(P, 1)[1].int()
40 |
41 |
--------------------------------------------------------------------------------
/auxiliary/ChamferDistancePytorch/fscore.py:
--------------------------------------------------------------------------------
1 | import torch
2 |
3 | def fscore(dist1, dist2, threshold=0.001):
4 | """
5 | Calculates the F-score between two point clouds with the corresponding threshold value.
6 | :param dist1: Batch, N-Points
7 | :param dist2: Batch, N-Points
8 | :param th: float
9 | :return: fscore, precision, recall
10 | """
11 |
12 | precision_1 = torch.mean((dist1 < threshold).float(), dim=1)
13 | precision_2 = torch.mean((dist2 < threshold).float(), dim=1)
14 | fscore = 2 * precision_1 * precision_2 / (precision_1 + precision_2)
15 | fscore[torch.isnan(fscore)] = 0
16 | return fscore, precision_1, precision_2
17 |
18 |
--------------------------------------------------------------------------------
/auxiliary/ChamferDistancePytorch/unit_test.py:
--------------------------------------------------------------------------------
1 | import torch, time
2 | import chamfer2D.dist_chamfer_2D
3 | import chamfer3D.dist_chamfer_3D
4 | import chamfer5D.dist_chamfer_5D
5 | import chamfer_python
6 |
7 | cham2D = chamfer2D.dist_chamfer_2D.chamfer_2DDist()
8 | cham3D = chamfer3D.dist_chamfer_3D.chamfer_3DDist()
9 | cham5D = chamfer5D.dist_chamfer_5D.chamfer_5DDist()
10 |
11 | from torch.autograd import Variable
12 | from fscore import fscore
13 |
14 | def test_chamfer(distChamfer, dim):
15 | points1 = torch.rand(4, 100, dim).cuda()
16 | points2 = torch.rand(4, 200, dim, requires_grad=True).cuda()
17 | dist1, dist2, idx1, idx2= distChamfer(points1, points2)
18 |
19 | loss = torch.sum(dist1)
20 | loss.backward()
21 |
22 | mydist1, mydist2, myidx1, myidx2 = chamfer_python.distChamfer(points1, points2)
23 | d1 = (dist1 - mydist1) ** 2
24 | d2 = (dist2 - mydist2) ** 2
25 | assert (
26 | torch.mean(d1) + torch.mean(d2) < 0.00000001
27 | ), "chamfer cuda and chamfer normal are not giving the same results"
28 |
29 | xd1 = idx1 - myidx1
30 | xd2 = idx2 - myidx2
31 | assert (
32 | torch.norm(xd1.float()) + torch.norm(xd2.float()) == 0
33 | ), "chamfer cuda and chamfer normal are not giving the same results"
34 | print(f"fscore :", fscore(dist1, dist2))
35 | print("Unit test passed")
36 |
37 |
38 | def timings(distChamfer, dim):
39 | p1 = torch.rand(32, 2000, dim).cuda()
40 | p2 = torch.rand(32, 1000, dim).cuda()
41 | print("Timings : Start CUDA version")
42 | start = time.time()
43 | num_it = 100
44 | for i in range(num_it):
45 | points1 = Variable(p1, requires_grad=True)
46 | points2 = Variable(p2)
47 | mydist1, mydist2, idx1, idx2 = distChamfer(points1, points2)
48 | loss = torch.sum(mydist1)
49 | loss.backward()
50 | print(f"Ellapsed time forward backward is {(time.time() - start)/num_it} seconds.")
51 |
52 |
53 | print("Timings : Start Pythonic version")
54 | start = time.time()
55 | for i in range(num_it):
56 | points1 = Variable(p1, requires_grad=True)
57 | points2 = Variable(p2)
58 | mydist1, mydist2, idx1, idx2 = chamfer_python.distChamfer(points1, points2)
59 | loss = torch.sum(mydist1)
60 | loss.backward()
61 | print(f"Ellapsed time forward backward is {(time.time() - start)/num_it} seconds.")
62 |
63 |
64 |
65 | dims = [2,3,5]
66 | for i,cham in enumerate([cham2D, cham3D, cham5D]):
67 | print(f"testing Chamfer {dims[i]}D")
68 | test_chamfer(cham, dims[i])
69 | timings(cham, dims[i])
70 |
--------------------------------------------------------------------------------
/compile_chamfer_distance_op.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | cd ./auxiliary/ChamferDistancePytorch/chamfer3D
4 |
5 | # update paths
6 | export CUDA_HOME=/usr/local/cuda-10.1
7 | export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/cuda-10.1/lib64:/usr/local/cuda-10.1/extras/CUPTI/lib64
8 | export PATH=$PATH:$CUDA_HOME/bin
9 |
10 | # compile 3D-Chamfer Distance op
11 | python ./setup.py install
12 |
--------------------------------------------------------------------------------
/datasets/KITTI_mapping.txt:
--------------------------------------------------------------------------------
1 |
2 |
3 | 2011_09_26 2011_09_26_drive_0005_sync 0000000010
4 | 2011_09_26 2011_09_26_drive_0005_sync 0000000059
5 |
6 |
7 |
8 | 2011_09_26 2011_09_26_drive_0009_sync 0000000354
9 | 2011_09_26 2011_09_26_drive_0009_sync 0000000364
10 | 2011_09_26 2011_09_26_drive_0009_sync 0000000374
11 | 2011_09_26 2011_09_26_drive_0009_sync 0000000384
12 | 2011_09_26 2011_09_26_drive_0009_sync 0000000394
13 | 2011_09_26 2011_09_26_drive_0009_sync 0000000414
14 | 2011_09_26 2011_09_26_drive_0011_sync 0000000111
15 | 2011_09_26 2011_09_26_drive_0011_sync 0000000127
16 | 2011_09_26 2011_09_26_drive_0011_sync 0000000147
17 | 2011_09_26 2011_09_26_drive_0011_sync 0000000157
18 | 2011_09_26 2011_09_26_drive_0011_sync 0000000167
19 | 2011_09_26 2011_09_26_drive_0013_sync 0000000010
20 | 2011_09_26 2011_09_26_drive_0013_sync 0000000020
21 | 2011_09_26 2011_09_26_drive_0013_sync 0000000040
22 | 2011_09_26 2011_09_26_drive_0013_sync 0000000070
23 | 2011_09_26 2011_09_26_drive_0014_sync 0000000010
24 | 2011_09_26 2011_09_26_drive_0014_sync 0000000020
25 | 2011_09_26 2011_09_26_drive_0014_sync 0000000030
26 | 2011_09_26 2011_09_26_drive_0014_sync 0000000050
27 | 2011_09_26 2011_09_26_drive_0014_sync 0000000060
28 | 2011_09_26 2011_09_26_drive_0014_sync 0000000129
29 | 2011_09_26 2011_09_26_drive_0014_sync 0000000141
30 | 2011_09_26 2011_09_26_drive_0014_sync 0000000152
31 | 2011_09_26 2011_09_26_drive_0014_sync 0000000172
32 | 2011_09_26 2011_09_26_drive_0014_sync 0000000192
33 | 2011_09_26 2011_09_26_drive_0014_sync 0000000213
34 | 2011_09_26 2011_09_26_drive_0014_sync 0000000240
35 | 2011_09_26 2011_09_26_drive_0015_sync 0000000187
36 | 2011_09_26 2011_09_26_drive_0015_sync 0000000197
37 | 2011_09_26 2011_09_26_drive_0015_sync 0000000209
38 | 2011_09_26 2011_09_26_drive_0015_sync 0000000219
39 | 2011_09_26 2011_09_26_drive_0015_sync 0000000229
40 | 2011_09_26 2011_09_26_drive_0015_sync 0000000239
41 | 2011_09_26 2011_09_26_drive_0015_sync 0000000264
42 | 2011_09_26 2011_09_26_drive_0015_sync 0000000273
43 | 2011_09_26 2011_09_26_drive_0015_sync 0000000286
44 | 2011_09_26 2011_09_26_drive_0017_sync 0000000010
45 | 2011_09_26 2011_09_26_drive_0017_sync 0000000030
46 | 2011_09_26 2011_09_26_drive_0017_sync 0000000040
47 | 2011_09_26 2011_09_26_drive_0017_sync 0000000050
48 | 2011_09_26 2011_09_26_drive_0018_sync 0000000046
49 | 2011_09_26 2011_09_26_drive_0018_sync 0000000066
50 | 2011_09_26 2011_09_26_drive_0018_sync 0000000076
51 | 2011_09_26 2011_09_26_drive_0018_sync 0000000086
52 | 2011_09_26 2011_09_26_drive_0018_sync 0000000096
53 | 2011_09_26 2011_09_26_drive_0018_sync 0000000106
54 | 2011_09_26 2011_09_26_drive_0018_sync 0000000133
55 | 2011_09_26 2011_09_26_drive_0019_sync 0000000030
56 | 2011_09_26 2011_09_26_drive_0019_sync 0000000087
57 | 2011_09_26 2011_09_26_drive_0019_sync 0000000097
58 | 2011_09_26 2011_09_26_drive_0022_sync 0000000634
59 | 2011_09_26 2011_09_26_drive_0022_sync 0000000644
60 | 2011_09_26 2011_09_26_drive_0022_sync 0000000654
61 | 2011_09_26 2011_09_26_drive_0027_sync 0000000053
62 | 2011_09_26 2011_09_26_drive_0027_sync 0000000103
63 | 2011_09_26 2011_09_26_drive_0028_sync 0000000071
64 | 2011_09_26 2011_09_26_drive_0028_sync 0000000118
65 | 2011_09_26 2011_09_26_drive_0028_sync 0000000228
66 | 2011_09_26 2011_09_26_drive_0028_sync 0000000269
67 | 2011_09_26 2011_09_26_drive_0028_sync 0000000284
68 | 2011_09_26 2011_09_26_drive_0028_sync 0000000303
69 | 2011_09_26 2011_09_26_drive_0028_sync 0000000313
70 | 2011_09_26 2011_09_26_drive_0028_sync 0000000378
71 | 2011_09_26 2011_09_26_drive_0029_sync 0000000016
72 | 2011_09_26 2011_09_26_drive_0029_sync 0000000123
73 | 2011_09_26 2011_09_26_drive_0032_sync 0000000095
74 | 2011_09_26 2011_09_26_drive_0032_sync 0000000114
75 | 2011_09_26 2011_09_26_drive_0032_sync 0000000125
76 | 2011_09_26 2011_09_26_drive_0032_sync 0000000207
77 | 2011_09_26 2011_09_26_drive_0032_sync 0000000218
78 | 2011_09_26 2011_09_26_drive_0032_sync 0000000330
79 | 2011_09_26 2011_09_26_drive_0032_sync 0000000340
80 | 2011_09_26 2011_09_26_drive_0032_sync 0000000350
81 | 2011_09_26 2011_09_26_drive_0032_sync 0000000360
82 | 2011_09_26 2011_09_26_drive_0032_sync 0000000378
83 |
84 | 2011_09_26 2011_09_26_drive_0036_sync 0000000054
85 | 2011_09_26 2011_09_26_drive_0036_sync 0000000402
86 | 2011_09_26 2011_09_26_drive_0046_sync 0000000052
87 | 2011_09_26 2011_09_26_drive_0046_sync 0000000062
88 |
89 | 2011_09_26 2011_09_26_drive_0051_sync 0000000023
90 | 2011_09_26 2011_09_26_drive_0051_sync 0000000218
91 | 2011_09_26 2011_09_26_drive_0051_sync 0000000230
92 | 2011_09_26 2011_09_26_drive_0051_sync 0000000282
93 | 2011_09_26 2011_09_26_drive_0051_sync 0000000292
94 | 2011_09_26 2011_09_26_drive_0051_sync 0000000302
95 | 2011_09_26 2011_09_26_drive_0051_sync 0000000312
96 | 2011_09_26 2011_09_26_drive_0051_sync 0000000322
97 | 2011_09_26 2011_09_26_drive_0051_sync 0000000342
98 | 2011_09_26 2011_09_26_drive_0051_sync 0000000356
99 | 2011_09_26 2011_09_26_drive_0051_sync 0000000379
100 |
101 |
102 |
103 |
104 |
105 |
106 | 2011_09_26 2011_09_26_drive_0056_sync 0000000010
107 | 2011_09_26 2011_09_26_drive_0056_sync 0000000082
108 | 2011_09_26 2011_09_26_drive_0056_sync 0000000122
109 | 2011_09_26 2011_09_26_drive_0056_sync 0000000132
110 | 2011_09_26 2011_09_26_drive_0056_sync 0000000191
111 | 2011_09_26 2011_09_26_drive_0056_sync 0000000201
112 | 2011_09_26 2011_09_26_drive_0056_sync 0000000282
113 | 2011_09_26 2011_09_26_drive_0057_sync 0000000125
114 | 2011_09_26 2011_09_26_drive_0057_sync 0000000140
115 | 2011_09_26 2011_09_26_drive_0057_sync 0000000176
116 | 2011_09_26 2011_09_26_drive_0057_sync 0000000299
117 | 2011_09_26 2011_09_26_drive_0057_sync 0000000319
118 | 2011_09_26 2011_09_26_drive_0057_sync 0000000339
119 | 2011_09_26 2011_09_26_drive_0059_sync 0000000026
120 | 2011_09_26 2011_09_26_drive_0059_sync 0000000046
121 | 2011_09_26 2011_09_26_drive_0059_sync 0000000137
122 | 2011_09_26 2011_09_26_drive_0059_sync 0000000150
123 | 2011_09_26 2011_09_26_drive_0059_sync 0000000260
124 | 2011_09_26 2011_09_26_drive_0059_sync 0000000280
125 | 2011_09_26 2011_09_26_drive_0059_sync 0000000290
126 | 2011_09_26 2011_09_26_drive_0059_sync 0000000300
127 | 2011_09_26 2011_09_26_drive_0059_sync 0000000310
128 | 2011_09_26 2011_09_26_drive_0059_sync 0000000320
129 | 2011_09_26 2011_09_26_drive_0070_sync 0000000069
130 | 2011_09_26 2011_09_26_drive_0070_sync 0000000224
131 | 2011_09_26 2011_09_26_drive_0084_sync 0000000084
132 | 2011_09_26 2011_09_26_drive_0084_sync 0000000179
133 | 2011_09_26 2011_09_26_drive_0084_sync 0000000238
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 | 2011_09_26 2011_09_26_drive_0096_sync 0000000020
143 | 2011_09_26 2011_09_26_drive_0096_sync 0000000278
144 | 2011_09_26 2011_09_26_drive_0096_sync 0000000381
145 | 2011_09_26 2011_09_26_drive_0101_sync 0000000109
146 | 2011_09_26 2011_09_26_drive_0101_sync 0000000175
147 | 2011_09_26 2011_09_26_drive_0101_sync 0000000447
148 | 2011_09_26 2011_09_26_drive_0101_sync 0000000457
149 | 2011_09_26 2011_09_26_drive_0101_sync 0000000809
150 | 2011_09_26 2011_09_26_drive_0104_sync 0000000015
151 | 2011_09_26 2011_09_26_drive_0104_sync 0000000035
152 |
153 |
154 |
155 |
156 | 2011_09_28 2011_09_28_drive_0002_sync 0000000343
157 |
158 | 2011_09_29 2011_09_29_drive_0004_sync 0000000036
159 | 2011_09_29 2011_09_29_drive_0004_sync 0000000079
160 | 2011_09_29 2011_09_29_drive_0004_sync 0000000094
161 | 2011_09_29 2011_09_29_drive_0004_sync 0000000105
162 | 2011_09_29 2011_09_29_drive_0004_sync 0000000162
163 | 2011_09_29 2011_09_29_drive_0004_sync 0000000258
164 | 2011_09_29 2011_09_29_drive_0004_sync 0000000285
165 | 2011_09_29 2011_09_29_drive_0004_sync 0000000308
166 |
167 |
168 |
169 | 2011_09_29 2011_09_29_drive_0071_sync 0000000059
170 | 2011_09_29 2011_09_29_drive_0071_sync 0000000943
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 | 2011_10_03 2011_10_03_drive_0047_sync 0000000556
201 |
--------------------------------------------------------------------------------
/datasets/flyingthings3d_flownet3d.py:
--------------------------------------------------------------------------------
1 | import os
2 | import glob
3 | import numpy as np
4 | from .generic import SceneFlowDataset
5 |
6 |
7 | class FT3D(SceneFlowDataset):
8 | def __init__(self, root_dir, nb_points, all_points, mode, nb_examples):
9 | """
10 | Construct the FlyingThing3D datatset as in:
11 | Liu, X., Qi, C.R., Guibas, L.J.: FlowNet3D: Learning scene flow in 3D
12 | point clouds. IEEE Conf. Computer Vision and Pattern Recognition
13 | (CVPR). pp. 529–537 (2019)
14 |
15 | Parameters
16 | ----------
17 | root_dir : str
18 | Path to root directory containing the datasets.
19 | nb_points : int
20 | Maximum number of points in point clouds.
21 | all_points : bool
22 | Whether to use all point in the point cloud (in chucks of nb_points) or only nb_points.
23 | mode : str
24 | 'train': training dataset.
25 |
26 | 'val': validation dataset.
27 |
28 | 'test': test dataset
29 | nb_examples: int
30 | Number of examples for the dataset. Active if > 0.
31 |
32 | """
33 |
34 | super(FT3D, self).__init__(nb_points, all_points)
35 | self.mode = mode
36 | self.nb_examples = nb_examples
37 | self.root_dir = root_dir
38 | self.filenames = self.get_file_list()
39 | self.filename_curr = ""
40 |
41 | def __len__(self):
42 |
43 | return len(self.filenames)
44 |
45 | def get_file_list(self):
46 | """
47 | Find and filter out paths to all examples in the dataset.
48 |
49 | """
50 |
51 | #
52 | if self.mode == "train" or self.mode == "val":
53 | pattern = "TRAIN_*.npz"
54 | elif self.mode == "test":
55 | pattern = "TEST_*.npz"
56 | else:
57 | raise ValueError("Mode " + str(self.mode) + "unknown.")
58 | filenames = glob.glob(os.path.join(self.root_dir, pattern))
59 |
60 | # Remove one sample containing a nan value in train set
61 | scan_with_nan_value = os.path.join(
62 | self.root_dir, "TRAIN_C_0140_left_0006-0.npz"
63 | )
64 | if scan_with_nan_value in filenames:
65 | filenames.remove(scan_with_nan_value)
66 |
67 | # Remove samples with all points occluded in train set
68 | scan_with_points_all_occluded = [
69 | "TRAIN_A_0364_left_0008-0.npz",
70 | "TRAIN_A_0364_left_0009-0.npz",
71 | "TRAIN_A_0658_left_0014-0.npz",
72 | "TRAIN_B_0053_left_0009-0.npz",
73 | "TRAIN_B_0053_left_0011-0.npz",
74 | "TRAIN_B_0424_left_0011-0.npz",
75 | "TRAIN_B_0609_right_0010-0.npz",
76 | ]
77 | for f in scan_with_points_all_occluded:
78 | if os.path.join(self.root_dir, f) in filenames:
79 | filenames.remove(os.path.join(self.root_dir, f))
80 |
81 | # Remove samples with all points occluded in test set
82 | scan_with_points_all_occluded = [
83 | "TEST_A_0149_right_0013-0.npz",
84 | "TEST_A_0149_right_0012-0.npz",
85 | "TEST_A_0123_right_0009-0.npz",
86 | "TEST_A_0123_right_0008-0.npz",
87 | ]
88 | for f in scan_with_points_all_occluded:
89 | if os.path.join(self.root_dir, f) in filenames:
90 | filenames.remove(os.path.join(self.root_dir, f))
91 |
92 | # Train / val / test split
93 | if self.mode == "train" or self.mode == "val":
94 | ind_val = set(np.linspace(0, len(filenames) - 1, 2000).astype("int"))
95 | ind_all = set(np.arange(len(filenames)).astype("int"))
96 | ind_train = ind_all - ind_val
97 | assert (
98 | len(ind_train.intersection(ind_val)) == 0
99 | ), "Train / Val not split properly"
100 | filenames = np.sort(filenames)
101 | if self.mode == "train":
102 | filenames = filenames[list(ind_train)]
103 | elif self.mode == "val":
104 | filenames = filenames[list(ind_val)]
105 |
106 | if 0 < self.nb_examples < len(filenames):
107 | idx_perm = np.random.permutation(len(filenames))
108 | idx_sel = idx_perm[:self.nb_examples]
109 | filenames = filenames[idx_sel]
110 |
111 | return filenames
112 |
113 | def load_sequence(self, idx):
114 | """
115 | Load a sequence of point clouds.
116 |
117 | Parameters
118 | ----------
119 | idx : int
120 | Index of the sequence to load.
121 |
122 | Returns
123 | -------
124 | sequence : list(np.array, np.array)
125 | List [pc1, pc2] of point clouds between which to estimate scene
126 | flow. pc1 has size n x 3 and pc2 has size m x 3.
127 |
128 | ground_truth : list(np.array, np.array)
129 | List [mask, flow]. mask has size n x 1 and pc1 has size n x 3.
130 | flow is the ground truth scene flow between pc1 and pc2. mask is
131 | binary with zeros indicating where the flow is not valid/occluded.
132 |
133 | """
134 |
135 | # Load data
136 | self.filename_curr = self.filenames[idx]
137 | with np.load(self.filename_curr) as data:
138 | sequence = [data["points1"], data["points2"]]
139 | ground_truth = [data["valid_mask1"].reshape(-1, 1), data["flow"]]
140 |
141 | return sequence, ground_truth
142 |
--------------------------------------------------------------------------------
/datasets/flyingthings3d_hplflownet.py:
--------------------------------------------------------------------------------
1 | import os
2 | import glob
3 | import numpy as np
4 | from .generic import SceneFlowDataset
5 |
6 |
7 | class FT3D(SceneFlowDataset):
8 | def __init__(self, root_dir, nb_points, all_points, mode, nb_examples):
9 | """
10 | Construct the FlyingThing3D datatset as in:
11 | Gu, X., Wang, Y., Wu, C., Lee, Y.J., Wang, P., HPLFlowNet: Hierarchical
12 | Permutohedral Lattice FlowNet for scene flow estimation on large-scale
13 | point clouds. IEEE Conf. Computer Vision and Pattern Recognition
14 | (CVPR). pp. 3254–3263 (2019)
15 |
16 | Parameters
17 | ----------
18 | root_dir : str
19 | Path to root directory containing the datasets.
20 | nb_points : int
21 | Maximum number of points in point clouds.
22 | all_points : bool
23 | Whether to use all point in the point cloud (in chucks of nb_points) or only nb_points.
24 | mode : str
25 | 'train': training dataset.
26 |
27 | 'val': validation dataset.
28 |
29 | 'test': test dataset
30 | nb_examples: int
31 | Number of examples for the dataset. Active if > 0.
32 |
33 | """
34 |
35 | super(FT3D, self).__init__(nb_points, all_points)
36 |
37 | self.mode = mode
38 | self.nb_examples = nb_examples
39 | self.root_dir = root_dir
40 | self.filenames = self.get_file_list()
41 | self.filename_curr = ""
42 |
43 | def __len__(self):
44 |
45 | return len(self.filenames)
46 |
47 | def get_file_list(self):
48 | """
49 | Find and filter out paths to all examples in the dataset.
50 |
51 | """
52 |
53 | # Get list of filenames / directories
54 | if self.mode == "train" or self.mode == "val":
55 | pattern = "train/0*"
56 | elif self.mode == "test":
57 | pattern = "val/0*"
58 | else:
59 | raise ValueError("Mode " + str(self.mode) + " unknown.")
60 | filenames = glob.glob(os.path.join(self.root_dir, pattern))
61 |
62 | # Train / val / test split
63 | if self.mode == "train" or self.mode == "val":
64 | assert len(filenames) == 19640, "Problem with size of training set"
65 | ind_val = set(np.linspace(0, 19639, 2000).astype("int"))
66 | ind_all = set(np.arange(19640).astype("int"))
67 | ind_train = ind_all - ind_val
68 | assert (
69 | len(ind_train.intersection(ind_val)) == 0
70 | ), "Train / Val not split properly"
71 | filenames = np.sort(filenames)
72 | if self.mode == "train":
73 | filenames = filenames[list(ind_train)]
74 | elif self.mode == "val":
75 | filenames = filenames[list(ind_val)]
76 | else:
77 | assert len(filenames) == 3824, "Problem with size of test set"
78 |
79 | if 0 < self.nb_examples < len(filenames):
80 | idx_perm = np.random.permutation(len(filenames))
81 | idx_sel = idx_perm[:self.nb_examples]
82 | filenames = filenames[idx_sel]
83 |
84 | return filenames
85 |
86 | def load_sequence(self, idx):
87 | """
88 | Load a sequence of point clouds.
89 |
90 | Parameters
91 | ----------
92 | idx : int
93 | Index of the sequence to load.
94 |
95 | Returns
96 | -------
97 | sequence : list(np.array, np.array)
98 | List [pc1, pc2] of point clouds between which to estimate scene
99 | flow. pc1 has size n x 3 and pc2 has size m x 3.
100 |
101 | ground_truth : list(np.array, np.array)
102 | List [mask, flow]. mask has size n x 1 and pc1 has size n x 3.
103 | flow is the ground truth scene flow between pc1 and pc2. mask is
104 | binary with zeros indicating where the flow is not valid/occluded.
105 |
106 | """
107 |
108 | # Load data
109 | self.filename_curr = self.filenames[idx]
110 | sequence = [] # [Point cloud 1, Point cloud 2]
111 | for fname in ["pc1.npy", "pc2.npy"]:
112 | pc = np.load(os.path.join(self.filename_curr, fname))
113 | pc[..., 0] *= -1
114 | pc[..., -1] *= -1
115 | sequence.append(pc)
116 | ground_truth = [
117 | np.ones_like(sequence[0][:, 0:1]),
118 | sequence[1] - sequence[0],
119 | ] # [Occlusion mask, flow]
120 |
121 | return sequence, ground_truth
122 |
--------------------------------------------------------------------------------
/datasets/generic.py:
--------------------------------------------------------------------------------
1 | import torch
2 | import numpy as np
3 | from torch.utils.data import Dataset
4 |
5 |
6 | class Batch:
7 | def __init__(self, batch):
8 | """
9 | Concatenate list of dataset.generic.SceneFlowDataset's item in batch
10 | dimension.
11 |
12 | Parameters
13 | ----------
14 | batch : list
15 | list of dataset.generic.SceneFlowDataset's item.
16 |
17 | """
18 |
19 | self.data = {}
20 | batch_size = len(batch)
21 | for key in ["sequence", "ground_truth", "orig_size"]:
22 | self.data[key] = []
23 | for ind_seq in range(len(batch[0][key])):
24 | tmp = []
25 | for ind_batch in range(batch_size):
26 | item = batch[ind_batch][key][ind_seq]
27 | if len(item.shape) > 3:
28 | tmp.append(item.reshape([-1, item.shape[-2], item.shape[-1]]))
29 | else:
30 | tmp.append(item)
31 | self.data[key].append(torch.cat(tmp, 0))
32 |
33 | def __getitem__(self, item):
34 | """
35 | Get 'sequence' or 'ground_truth' from the batch.
36 |
37 | Parameters
38 | ----------
39 | item : str
40 | Accept two keys 'sequence' or 'ground_truth'.
41 |
42 | Returns
43 | -------
44 | list(torch.Tensor, torch.Tensor)
45 | item='sequence': returns a list [pc1, pc2] of point clouds between
46 | which to estimate scene flow. pc1 has size B x n x 3 and pc2 has
47 | size B x m x 3.
48 |
49 | item='ground_truth': returns a list [mask, flow]. mask has size
50 | B x n x 1 and flow has size B x n x 3. flow is the ground truth
51 | scene flow between pc1 and pc2. flow is the ground truth scene
52 | flow. mask is binary with zeros indicating where the flow is not
53 | valid or occluded.
54 |
55 | """
56 | return self.data[item]
57 |
58 | def to(self, *args, **kwargs):
59 |
60 | for key in self.data.keys():
61 | self.data[key] = [d.to(*args, **kwargs) for d in self.data[key]]
62 |
63 | return self
64 |
65 | def pin_memory(self):
66 |
67 | for key in self.data.keys():
68 | self.data[key] = [d.pin_memory() for d in self.data[key]]
69 |
70 | return self
71 |
72 |
73 | class SceneFlowDataset(Dataset):
74 | def __init__(self, nb_points, all_points=False):
75 | """
76 | Abstract constructor for scene flow datasets.
77 |
78 | Each item of the dataset is returned in a dictionary with two keys:
79 | (key = 'sequence', value=list(torch.Tensor, torch.Tensor)):
80 | list [pc1, pc2] of point clouds between which to estimate scene
81 | flow. pc1 has size 1 x n x 3 and pc2 has size 1 x m x 3.
82 |
83 | (key = 'ground_truth', value = list(torch.Tensor, torch.Tensor)):
84 | list [mask, flow]. mask has size 1 x n x 1 and flow has size
85 | 1 x n x 3. flow is the ground truth scene flow between pc1 and pc2.
86 | mask is binary with zeros indicating where the flow is not
87 | valid/occluded.
88 |
89 | Parameters
90 | ----------
91 | nb_points : int
92 | Maximum number of points in point clouds: self.nb_points <= m, n.
93 | all_points : bool
94 | Whether to use all point in the point cloud (in chucks of nb_points) or only nb_points.
95 |
96 | """
97 |
98 | super(SceneFlowDataset, self).__init__()
99 | self.nb_points = nb_points
100 | self.all_points = all_points
101 |
102 | def __getitem__(self, idx):
103 | sequence, ground_truth, orig_size = self.to_torch(*self.subsample_points_rnd(*self.load_sequence(idx)))
104 | data = {"sequence": sequence, "ground_truth": ground_truth, "orig_size": orig_size}
105 |
106 | return data
107 |
108 | def to_torch(self, sequence, ground_truth, orig_size):
109 | """
110 | Convert numpy array and torch.Tensor.
111 |
112 | Parameters
113 | ----------
114 | sequence : list(np.array, np.array)
115 | List [pc1, pc2] of point clouds between which to estimate scene
116 | flow. pc1 has size n x 3 and pc2 has size m x 3.
117 |
118 | ground_truth : list(np.array, np.array)
119 | List [mask, flow]. mask has size n x 1 and pc1 has size n x 3.
120 | flow is the ground truth scene flow between pc1 and pc2. mask is
121 | binary with zeros indicating where the flow is not valid/occluded.
122 |
123 | orig_size : list(np.array, np.array)
124 | List [n1, n2]. Original size of the point clouds.
125 |
126 | Returns
127 | -------
128 | sequence : list(torch.Tensor, torch.Tensor)
129 | List [pc1, pc2] of point clouds between which to estimate scene
130 | flow. pc1 has size 1 x n x 3 and pc2 has size 1 x m x 3.
131 |
132 | ground_truth : list(torch.Tensor, torch.Tensor)
133 | List [mask, flow]. mask has size 1 x n x 1 and pc1 has size
134 | 1 x n x 3. flow is the ground truth scene flow between pc1 and pc2.
135 | mask is binary with zeros indicating where the flow is not
136 | valid/occluded.
137 |
138 | """
139 |
140 | sequence = [torch.unsqueeze(torch.from_numpy(s), 0).float() for s in sequence]
141 | ground_truth = [torch.unsqueeze(torch.from_numpy(gt), 0).float() for gt in ground_truth]
142 | orig_size = [torch.unsqueeze(torch.from_numpy(os), 0) for os in orig_size]
143 |
144 | return sequence, ground_truth, orig_size
145 |
146 | def subsample_points_rnd(self, sequence, ground_truth):
147 | """
148 | Subsample point clouds randomly.
149 |
150 | Parameters
151 | ----------
152 | sequence : list(np.array, np.array)
153 | List [pc1, pc2] of point clouds between which to estimate scene
154 | flow. pc1 has size 1 x N x 3 and pc2 has size 1 x M x 3.
155 |
156 | ground_truth : list(np.array, np.array)
157 | List [mask, flow]. mask has size 1 x N x 1 and flow has size
158 | 1 x N x 3. flow is the ground truth scene flow between pc1 and pc2.
159 | mask is binary with zeros indicating where the flow is not
160 | valid/occluded.
161 |
162 | Returns
163 | -------
164 | sequence : list(np.array, np.array)
165 | List [pc1, pc2] of point clouds between which to estimate scene
166 | flow. pc1 has size 1 x n x 3 and pc2 has size 1 x m x 3. The n
167 | points are chosen randomly among the N available ones. The m points
168 | are chosen randomly among the M available ones. If N, M >=
169 | self.nb_point then n, m = self.nb_points. If N, M <
170 | self.nb_point then n, m = N, M.
171 |
172 | ground_truth : list(np.array, np.array)
173 | List [mask, flow]. mask has size 1 x n x 1 and flow has size
174 | 1 x n x 3. flow is the ground truth scene flow between pc1 and pc2.
175 | mask is binary with zeros indicating where the flow is not
176 | valid/occluded.
177 |
178 | """
179 |
180 | # Permute indices
181 | n1 = sequence[0].shape[0]
182 | idx1_perm = np.random.permutation(n1)
183 |
184 | n2 = sequence[1].shape[0]
185 | idx2_perm = np.random.permutation(n2)
186 |
187 | # Prepare indices for sampling
188 | if self.all_points:
189 | if self.nb_points == 1:
190 | idx1 = np.arange(n1)
191 | idx2 = np.arange(n2)
192 | else:
193 | n1_div_ceil = n1 // self.nb_points + int((n1 % self.nb_points) > 0)
194 | n1_ceil = n1_div_ceil * self.nb_points
195 |
196 | n2_div_ceil = n2 // self.nb_points + int((n2 % self.nb_points) > 0)
197 | n2_ceil = n2_div_ceil * self.nb_points
198 |
199 | # Take larger point cloud size, in order to have all point from both point clouds for evaluation
200 | n_ceil = n1_ceil > n2_ceil and n1_ceil or n2_ceil
201 |
202 | idx1 = np.concatenate([idx1_perm, idx1_perm[:(n_ceil - n1)]])
203 | idx2 = np.concatenate([idx2_perm, idx2_perm[:(n_ceil - n2)]])
204 | else:
205 | idx1 = idx1_perm[:self.nb_points]
206 | idx2 = idx2_perm[:self.nb_points]
207 |
208 | # Sample points in the first scan
209 | sequence[0] = sequence[0][idx1]
210 | ground_truth = [g[idx1] for g in ground_truth]
211 |
212 | # Sample point in the second scan
213 | sequence[1] = sequence[1][idx2]
214 |
215 | # Reshape data
216 | if self.all_points:
217 | sequence[0] = sequence[0].reshape([-1, self.nb_points, 3])
218 | ground_truth = [g.reshape([-1, self.nb_points, g.shape[1]]) for g in ground_truth]
219 |
220 | sequence[1] = sequence[1].reshape([-1, self.nb_points, 3])
221 |
222 | if self.nb_points == 1:
223 | sequence = [s.transpose(1, 0, 2) for s in sequence]
224 | ground_truth = [g.transpose(1, 0, 2) for g in ground_truth]
225 |
226 | orig_size = [np.array([n1], dtype=np.int32), np.array([n2], dtype=np.int32)]
227 |
228 | return sequence, ground_truth, orig_size
229 |
230 | def load_sequence(self, idx):
231 | """
232 | Abstract function to be implemented to load a sequence of point clouds.
233 |
234 | Parameters
235 | ----------
236 | idx : int
237 | Index of the sequence to load.
238 |
239 | Must return:
240 | -------
241 | sequence : list(np.array, np.array)
242 | List [pc1, pc2] of point clouds between which to estimate scene
243 | flow. pc1 has size N x 3 and pc2 has size M x 3.
244 |
245 | ground_truth : list(np.array, np.array)
246 | List [mask, flow]. mask has size N x 1 and flow has size N x 3.
247 | flow is the ground truth scene flow between pc1 and pc2. mask is
248 | binary with zeros indicating where the flow is not valid/occluded.
249 |
250 | """
251 |
252 | raise NotImplementedError
253 |
--------------------------------------------------------------------------------
/datasets/kitti_flownet3d.py:
--------------------------------------------------------------------------------
1 | import os
2 | import glob
3 | import numpy as np
4 | from .generic import SceneFlowDataset
5 |
6 |
7 | class Kitti(SceneFlowDataset):
8 | def __init__(self, root_dir, nb_points, all_points, same_v_t_split, mode):
9 | """
10 | Construct the KITTI scene flow datatset as in:
11 | Liu, X., Qi, C.R., Guibas, L.J.: FlowNet3D: Learning scene flow in 3D
12 | point clouds. IEEE Conf. Computer Vision and Pattern Recognition
13 | (CVPR). pp. 529–537 (2019)
14 |
15 | Parameters
16 | ----------
17 | root_dir : str
18 | Path to root directory containing the datasets.
19 | nb_points : int
20 | Maximum number of points in point clouds.
21 | all_points : bool
22 | Whether to use all point in the point cloud (in chucks of nb_points) or only nb_points.
23 | same_v_t_split: bool
24 | Whether to use the same validation and test split.
25 | mode : str
26 | 'train': training dataset.
27 |
28 | 'val': validation dataset.
29 |
30 | 'test': test dataset
31 |
32 | 'all': all dataset
33 |
34 | """
35 |
36 | super(Kitti, self).__init__(nb_points, all_points)
37 | self.mode = mode
38 | self.root_dir = root_dir
39 | self.same_v_t_split = same_v_t_split
40 | self.filenames = self.make_dataset()
41 | self.filename_curr = ""
42 |
43 | def __len__(self):
44 |
45 | return len(self.filenames)
46 |
47 | def make_dataset(self):
48 | """
49 | Find and filter out paths to all examples in the dataset.
50 |
51 | """
52 | len_dataset = 150
53 | filenames_all = glob.glob(os.path.join(self.root_dir, "*.npz"))
54 |
55 | test_list = [1, 5, 7, 8, 10, 12, 15, 17, 20, 21, 24, 25, 29, 30, 31, 32, 34, 35, 36, 39, 40, 44, 45, 47, 48,
56 | 49, 50, 51, 53, 55, 56, 58, 59, 60, 70, 71, 72, 74, 76, 77, 78, 79, 81, 82, 88, 91, 93, 94, 95, 98]
57 | val_list = [4, 54, 73, 101, 102, 104, 115, 130, 136, 147]
58 |
59 | if self.same_v_t_split:
60 | val_list = test_list
61 |
62 | train_list = [i for i in range(len_dataset) if i not in test_list and i not in val_list]
63 |
64 | if self.mode == "train":
65 | filenames_train = [fn for fn in filenames_all if int(os.path.split(fn)[1].split(".")[0]) in train_list]
66 | train_size = 100 if self.same_v_t_split else 90
67 | assert len(filenames_train) == train_size, "Problem with size of kitti train dataset"
68 | filenames = filenames_train
69 |
70 | elif self.mode == "val":
71 | filenames_val = [fn for fn in filenames_all if int(os.path.split(fn)[1].split(".")[0]) in val_list]
72 | val_size = 50 if self.same_v_t_split else 10
73 | assert len(filenames_val) == val_size, "Problem with size of kitti validation dataset"
74 | filenames = filenames_val
75 |
76 | elif self.mode == "test":
77 | filenames_test = [fn for fn in filenames_all if int(os.path.split(fn)[1].split(".")[0]) in test_list]
78 | assert len(filenames_test) == 50, "Problem with size of kitti test dataset"
79 | filenames = filenames_test
80 |
81 | elif self.mode == "all":
82 | assert len(filenames_all) == 150, "Problem with size of kitti dataset"
83 | filenames = filenames_all
84 | else:
85 | raise ValueError("Mode " + str(self.mode) + "unknown.")
86 |
87 | return filenames
88 |
89 | def load_sequence(self, idx):
90 | """
91 | Load a sequence of point clouds.
92 |
93 | Parameters
94 | ----------
95 | idx : int
96 | Index of the sequence to load.
97 |
98 | Returns
99 | -------
100 | sequence : list(np.array, np.array)
101 | List [pc1, pc2] of point clouds between which to estimate scene
102 | flow. pc1 has size n x 3 and pc2 has size m x 3.
103 |
104 | ground_truth : list(np.array, np.array)
105 | List [mask, flow]. mask has size n x 1 and pc1 has size n x 3.
106 | flow is the ground truth scene flow between pc1 and pc2. mask is
107 | binary with zeros indicating where the flow is not valid/occluded.
108 |
109 | """
110 |
111 | # Load data
112 | self.filename_curr = self.filenames[idx]
113 | with np.load(self.filename_curr) as data:
114 | sequence = [data["pos1"][:, (1, 2, 0)], data["pos2"][:, (1, 2, 0)]]
115 | ground_truth = [
116 | np.ones_like(data["pos1"][:, 0:1]),
117 | data["gt"][:, (1, 2, 0)],
118 | ]
119 |
120 | # Restrict to 35m
121 | loc = sequence[0][:, 2] < 35
122 | sequence[0] = sequence[0][loc]
123 | ground_truth[0] = ground_truth[0][loc]
124 | ground_truth[1] = ground_truth[1][loc]
125 | loc = sequence[1][:, 2] < 35
126 | sequence[1] = sequence[1][loc]
127 |
128 | return sequence, ground_truth
129 |
--------------------------------------------------------------------------------
/datasets/kitti_hplflownet.py:
--------------------------------------------------------------------------------
1 | import os
2 | import numpy as np
3 | from .generic import SceneFlowDataset
4 |
5 |
6 | class Kitti(SceneFlowDataset):
7 | def __init__(self, root_dir, nb_points, all_points, mode):
8 | """
9 | Construct the KITTI scene flow datatset as in:
10 | Gu, X., Wang, Y., Wu, C., Lee, Y.J., Wang, P., HPLFlowNet: Hierarchical
11 | Permutohedral Lattice FlowNet for scene flow estimation on large-scale
12 | point clouds. IEEE Conf. Computer Vision and Pattern Recognition
13 | (CVPR). pp. 3254–3263 (2019)
14 |
15 | Parameters
16 | ----------
17 | root_dir : str
18 | Path to root directory containing the datasets.
19 | nb_points : int
20 | Maximum number of points in point clouds.
21 | all_points : bool
22 | Whether to use all point in the point cloud (in chucks of nb_points) or only nb_points.
23 | mode : str
24 | 'train': training dataset.
25 |
26 | 'val': validation dataset.
27 |
28 | 'test': test dataset
29 |
30 | 'all': all dataset
31 |
32 | """
33 |
34 | super(Kitti, self).__init__(nb_points, all_points)
35 | self.mode = mode
36 | self.root_dir = root_dir
37 | self.paths = self.make_dataset()
38 | self.filename_curr = ""
39 |
40 | def __len__(self):
41 |
42 | return len(self.paths)
43 |
44 | def make_dataset(self):
45 | """
46 | Find and filter out paths to all examples in the dataset.
47 |
48 | """
49 |
50 | #
51 | root = os.path.realpath(os.path.expanduser(self.root_dir))
52 | all_paths = sorted(os.walk(root))
53 | useful_paths = [item[0] for item in all_paths if len(item[1]) == 0]
54 | assert len(useful_paths) == 200, "Problem with size of kitti dataset"
55 |
56 | # Mapping / Filtering of scans as in HPLFlowNet code
57 | mapping_path = os.path.join(os.path.dirname(__file__), "KITTI_mapping.txt")
58 | with open(mapping_path) as fd:
59 | lines = fd.readlines()
60 | lines = [line.strip() for line in lines]
61 | useful_paths = [
62 | path for path in useful_paths if lines[int(os.path.split(path)[-1])] != ""
63 | ]
64 |
65 | useful_paths = np.array(useful_paths)
66 | len_dataset = len(useful_paths)
67 |
68 | # the train indices was randomly selected by:
69 | # train_idx = self.make_subset_idx(total_examples=len_dataset, nb_examples=92, seed=42)
70 | train_idx = [0, 3, 4, 5, 7, 8, 9, 10, 11, 12, 13, 15, 16, 17, 18, 19, 22, 23, 24, 25, 26, 27, 28, 30, 31, 33,
71 | 34, 35, 36, 39, 40, 42, 43, 44, 45, 47, 49, 51, 53, 55, 56, 60, 62, 64, 65, 66, 67, 68, 69, 70,
72 | 73, 76, 77, 78, 80, 81, 82, 83, 84, 85, 86, 89, 93, 94, 95, 96, 97, 98, 101, 105, 108, 109, 110,
73 | 111, 112, 113, 114, 115, 117, 118, 122, 123, 124, 126, 128, 130, 131, 133, 135, 137, 138, 140]
74 |
75 | test_idx = [i for i in range(len_dataset) if i not in train_idx]
76 | val_idx = test_idx
77 |
78 | if self.mode == "train":
79 | useful_paths_train = useful_paths[train_idx]
80 | train_size = 92
81 | assert len(useful_paths_train) == train_size, "Problem with size of kitti train dataset"
82 | useful_paths = useful_paths_train
83 |
84 | elif self.mode == "val":
85 | useful_paths_val = useful_paths[val_idx]
86 | val_size = 50
87 | assert len(useful_paths_val) == val_size, "Problem with size of kitti validation dataset"
88 | useful_paths = useful_paths_val
89 |
90 | elif self.mode == "test":
91 | useful_paths_test = useful_paths[test_idx]
92 | assert len(useful_paths_test) == 50, "Problem with size of kitti test dataset"
93 | useful_paths = useful_paths_test
94 |
95 | elif self.mode == "all":
96 | assert len(useful_paths) == 142, "Problem with size of kitti dataset"
97 | else:
98 | raise ValueError("Mode " + str(self.mode) + "unknown.")
99 |
100 | useful_paths = list(useful_paths)
101 |
102 | return useful_paths
103 |
104 | def make_subset_idx(self, total_examples, nb_examples, seed=42):
105 | np.random.seed(seed)
106 | idx_perm = np.random.permutation(total_examples)
107 | idx_sel = np.sort(idx_perm[:nb_examples])
108 |
109 | return idx_sel
110 |
111 | def load_sequence(self, idx):
112 | """
113 | Load a sequence of point clouds.
114 |
115 | Parameters
116 | ----------
117 | idx : int
118 | Index of the sequence to load.
119 |
120 | Returns
121 | -------
122 | sequence : list(np.array, np.array)
123 | List [pc1, pc2] of point clouds between which to estimate scene
124 | flow. pc1 has size n x 3 and pc2 has size m x 3.
125 |
126 | ground_truth : list(np.array, np.array)
127 | List [mask, flow]. mask has size n x 1 and pc1 has size n x 3.
128 | flow is the ground truth scene flow between pc1 and pc2. mask is
129 | binary with zeros indicating where the flow is not valid/occluded.
130 |
131 | """
132 |
133 | # Load data
134 | self.filename_curr = self.paths[idx]
135 | sequence = [np.load(os.path.join(self.paths[idx], "pc1.npy"))]
136 | sequence.append(np.load(os.path.join(self.paths[idx], "pc2.npy")))
137 |
138 | # Remove ground points
139 | is_ground = np.logical_and(sequence[0][:, 1] < -1.4, sequence[1][:, 1] < -1.4)
140 | not_ground = np.logical_not(is_ground)
141 | sequence = [sequence[i][not_ground] for i in range(2)]
142 |
143 | # Remove points further than 35 meter away as in HPLFlowNet code
144 | is_close = np.logical_and(sequence[0][:, 2] < 35, sequence[1][:, 2] < 35)
145 | sequence = [sequence[i][is_close] for i in range(2)]
146 |
147 | # Scene flow
148 | ground_truth = [
149 | np.ones_like(sequence[0][:, 0:1]),
150 | sequence[1] - sequence[0],
151 | ] # [Occlusion mask, scene flow]
152 |
153 | return sequence, ground_truth
154 |
--------------------------------------------------------------------------------
/doc/poster.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itailang/SCOOP/9f41e8baafed8689be867b8ab7b10159371de37d/doc/poster.png
--------------------------------------------------------------------------------
/doc/scoop_result.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itailang/SCOOP/9f41e8baafed8689be867b8ab7b10159371de37d/doc/scoop_result.gif
--------------------------------------------------------------------------------
/doc/slides.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itailang/SCOOP/9f41e8baafed8689be867b8ab7b10159371de37d/doc/slides.pdf
--------------------------------------------------------------------------------
/download_pretrained_models.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # define a download function
4 | function google_drive_download()
5 | {
6 | CONFIRM=$(wget --quiet --save-cookies /tmp/cookies.txt --keep-session-cookies --no-check-certificate "https://docs.google.com/uc?export=download&id=$1" -O- | sed -rn 's/.*confirm=([0-9A-Za-z_]+).*/\1\n/p')
7 | wget --load-cookies /tmp/cookies.txt "https://docs.google.com/uc?export=download&confirm=$CONFIRM&id=$1" -O $2
8 | rm -rf /tmp/cookies.txt
9 | }
10 |
11 | # download models
12 | google_drive_download 14M0R0AMJhsyGZYHDIp3p8QStCkKCy_bO pretrained_models.zip
13 | unzip pretrained_models.zip
14 | rm pretrained_models.zip
15 |
--------------------------------------------------------------------------------
/install_environment.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # install dependencies
4 | conda install pytorch==1.6.0 torchvision==0.7.0 cudatoolkit=10.1 -c pytorch --yes
5 | pip install tensorboard==2.4.1 --no-cache-dir
6 | pip install tqdm==4.60.0 --no-cache-dir
7 |
8 | # install mayavi for visualization
9 | pip install vtk==9.1.0 --no-cache-dir
10 | pip install pyQt5==5.15.2 --no-cache-dir
11 | pip install mayavi==4.7.4 --no-cache-dir
12 |
--------------------------------------------------------------------------------
/models/gconv.py:
--------------------------------------------------------------------------------
1 | import torch
2 |
3 |
4 | class SetConv(torch.nn.Module):
5 | def __init__(self, nb_feat_in, nb_feat_out):
6 | """
7 | Module that performs PointNet++-like convolution on point clouds.
8 |
9 | Parameters
10 | ----------
11 | nb_feat_in : int
12 | Number of input channels.
13 | nb_feat_out : int
14 | Number of ouput channels.
15 |
16 | Returns
17 | -------
18 | None.
19 |
20 | """
21 |
22 | super(SetConv, self).__init__()
23 |
24 | self.fc1 = torch.nn.Conv2d(nb_feat_in + 3, nb_feat_out, 1, bias=False)
25 | self.bn1 = torch.nn.InstanceNorm2d(nb_feat_out, affine=True)
26 |
27 | self.fc2 = torch.nn.Conv2d(nb_feat_out, nb_feat_out, 1, bias=False)
28 | self.bn2 = torch.nn.InstanceNorm2d(nb_feat_out, affine=True)
29 |
30 | self.fc3 = torch.nn.Conv2d(nb_feat_out, nb_feat_out, 1, bias=False)
31 | self.bn3 = torch.nn.InstanceNorm2d(nb_feat_out, affine=True)
32 |
33 | self.pool = lambda x: torch.max(x, 2)[0]
34 | self.lrelu = torch.nn.LeakyReLU(negative_slope=0.1)
35 |
36 | def forward(self, signal, graph):
37 | """
38 | Performs PointNet++-like convolution
39 |
40 | Parameters
41 | ----------
42 | signal : torch.Tensor
43 | Input features of size B x N x nb_feat_in.
44 | graph : scoop.models.graph.Graph
45 | Graph build on the input point cloud on with the input features
46 | live. The graph contains the list of nearest neighbors (NN) for
47 | each point and all edge features (relative point coordinates with
48 | NN).
49 |
50 | Returns
51 | -------
52 | torch.Tensor
53 | Ouput features of size B x N x nb_feat_out.
54 |
55 | """
56 |
57 | # Input features dimension
58 | b, n, c = signal.shape
59 | n_out = graph.size[0] // b
60 |
61 | # Concatenate input features with edge features
62 | signal = signal.reshape(b * n, c)
63 | signal = torch.cat((signal[graph.edges], graph.edge_feats), -1)
64 | signal = signal.view(b, n_out, graph.k_neighbors, c + 3)
65 | signal = signal.transpose(1, -1)
66 |
67 | # Pointnet++-like convolution
68 | for func in [
69 | self.fc1,
70 | self.bn1,
71 | self.lrelu,
72 | self.fc2,
73 | self.bn2,
74 | self.lrelu,
75 | self.fc3,
76 | self.bn3,
77 | self.lrelu,
78 | self.pool,
79 | ]:
80 | signal = func(signal)
81 |
82 | return signal.transpose(1, -1)
83 |
--------------------------------------------------------------------------------
/models/graph.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | import torch
4 | import numpy as np
5 |
6 | # add path
7 | project_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
8 | if project_dir not in sys.path:
9 | sys.path.append(project_dir)
10 |
11 | from tools.utils import iterate_in_chunks
12 |
13 | class Graph:
14 | def __init__(self, edges, edge_feats, k_neighbors, size):
15 | """
16 | Directed nearest neighbor graph constructed on a point cloud.
17 |
18 | Parameters
19 | ----------
20 | edges : torch.Tensor
21 | Contains list with nearest neighbor indices.
22 | edge_feats : torch.Tensor
23 | Contains edge features: relative point coordinates.
24 | k_neighbors : int
25 | Number of nearest neighbors.
26 | size : tuple(int, int)
27 | Number of points.
28 |
29 | """
30 |
31 | self.edges = edges
32 | self.size = tuple(size)
33 | self.edge_feats = edge_feats
34 | self.k_neighbors = k_neighbors
35 |
36 | @staticmethod
37 | def construct_graph(pcloud, nb_neighbors):
38 | """
39 | Construct a directed nearest neighbor graph on the input point cloud.
40 |
41 | Parameters
42 | ----------
43 | pcloud : torch.Tensor
44 | Input point cloud. Size B x N x 3.
45 | nb_neighbors : int
46 | Number of nearest neighbors per point.
47 |
48 | Returns
49 | -------
50 | graph : scoop.models.graph.Graph
51 | Graph build on input point cloud containing the list of nearest
52 | neighbors (NN) for each point and all edge features (relative
53 | coordinates with NN).
54 |
55 | """
56 |
57 | # Size
58 | nb_points = pcloud.shape[1]
59 | size_batch = pcloud.shape[0]
60 |
61 | # Distance between points
62 | distance_matrix = torch.sum(pcloud ** 2, -1, keepdim=True)
63 | distance_matrix = distance_matrix + distance_matrix.transpose(1, 2)
64 | distance_matrix = distance_matrix - 2 * torch.bmm(
65 | pcloud, pcloud.transpose(1, 2)
66 | )
67 |
68 | # Find nearest neighbors
69 | neighbors = torch.argsort(distance_matrix, -1)[..., :nb_neighbors]
70 | effective_nb_neighbors = neighbors.shape[-1]
71 | neighbors = neighbors.reshape(size_batch, -1)
72 |
73 | # Edge origin
74 | idx = torch.arange(nb_points, device=distance_matrix.device).long()
75 | idx = torch.repeat_interleave(idx, effective_nb_neighbors)
76 |
77 | # Edge features
78 | edge_feats = []
79 | for ind_batch in range(size_batch):
80 | edge_feats.append(
81 | pcloud[ind_batch, neighbors[ind_batch]] - pcloud[ind_batch, idx]
82 | )
83 | edge_feats = torch.cat(edge_feats, 0)
84 |
85 | # Handle batch dimension to get indices of nearest neighbors
86 | for ind_batch in range(1, size_batch):
87 | neighbors[ind_batch] = neighbors[ind_batch] + ind_batch * nb_points
88 | neighbors = neighbors.view(-1)
89 |
90 | # Create graph
91 | graph = Graph(
92 | neighbors,
93 | edge_feats,
94 | effective_nb_neighbors,
95 | [size_batch * nb_points, size_batch * nb_points],
96 | )
97 |
98 | return graph
99 |
100 | @staticmethod
101 | def construct_graph_in_chunks(pcloud, nb_neighbors, chunk_size):
102 | # Size
103 | size_batch, nb_points, _ = pcloud.shape
104 | assert size_batch == 1, "For construction of graph in chucks, the batch size should be 1, got %d." % size_batch
105 |
106 | # Find nearest neighbors
107 | #distance_matrix = -1 * torch.ones([nb_points, nb_points], dtype=torch.float32, device=pcloud.device)
108 | neighbors = -1 * torch.ones([nb_points, nb_neighbors], dtype=torch.int64, device=pcloud.device)
109 | idx = np.arange(nb_points)
110 | for b in iterate_in_chunks(idx, chunk_size):
111 | # Distance between points
112 | points_curr = torch.transpose(pcloud[:, b], 0, 1)
113 | #distance_matrix_curr = torch.sum(torch.pow(points_curr - pcloud, 2), -1)
114 | distance_matrix_curr = torch.sum(points_curr ** 2, -1) - 2 * torch.sum(points_curr * pcloud, dim=-1) + torch.sum(pcloud ** 2, -1)
115 | #distance_matrix[b, :] = distance_matrix_curr
116 |
117 | # Find nearest neighbors
118 | neighbors_curr = torch.argsort(distance_matrix_curr, -1)[..., :nb_neighbors]
119 | neighbors[b, :] = neighbors_curr
120 |
121 | assert torch.all(neighbors >= 0), "Problem with nearest neighbors computation. Not all indices filled correctly."
122 | #assert torch.all(distance_matrix >= 0), "Problem with distance matrix computation. Not all distances filled correctly."
123 |
124 | effective_nb_neighbors = neighbors.shape[-1]
125 | edge_feats = torch.empty(0)
126 |
127 | neighbors = neighbors.view(-1)
128 |
129 | # Create graph
130 | graph = Graph(
131 | neighbors,
132 | edge_feats,
133 | effective_nb_neighbors,
134 | [size_batch * nb_points, size_batch * nb_points],
135 | )
136 |
137 | return graph
138 |
--------------------------------------------------------------------------------
/models/refiner.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | import numpy as np
4 | import time
5 | import torch
6 |
7 | # add path
8 | project_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
9 | if project_dir not in sys.path:
10 | sys.path.append(project_dir)
11 |
12 | from models.graph import Graph
13 | from tools.losses import chamfer_loss, smooth_loss
14 |
15 |
16 | class Refiner(torch.nn.Module):
17 | def __init__(self, shape, device):
18 | """
19 | Construct a model for refining scene flow between two point clouds.
20 |
21 | Parameters
22 | ----------
23 | shape:
24 | Shape of the refinement tensor.
25 | device:
26 | Device of the refinement tensor.
27 | """
28 |
29 | super(Refiner, self).__init__()
30 |
31 | self.refinement = torch.nn.Parameter(torch.zeros(shape, dtype=torch.float32, device=device, requires_grad=True))
32 |
33 | def forward(self, flow):
34 | refined_flow = flow + self.refinement
35 |
36 | return refined_flow
37 |
38 | def refine_flow(self, batch, flow, corr_conf, optimizer, args):
39 | pc_0, pc_1 = batch["sequence"][0], batch["sequence"][1]
40 | gt_flow = batch["ground_truth"][1]
41 |
42 | n0 = int(batch["orig_size"][0].cpu().numpy())
43 | n1 = int(batch["orig_size"][1].cpu().numpy())
44 |
45 | b, nb_points0, c = pc_0.shape
46 | b, nb_points1, c = pc_1.shape
47 |
48 | pc_0_orig = torch.unsqueeze(torch.reshape(pc_0, (b * nb_points0, 3))[:n0], dim=0)
49 | pc_1_orig = torch.unsqueeze(torch.reshape(pc_1, (b * nb_points1, 3))[:n1], dim=0)
50 | gt_flow_orig = torch.unsqueeze(torch.reshape(gt_flow, (b * nb_points0, 3))[:n0], dim=0)
51 |
52 | start_time = time.time()
53 |
54 | graph = Graph.construct_graph_in_chunks(pc_0_orig, 32, 2048)
55 |
56 | # results aggregation
57 | target_recon_loss_all = np.zeros(args.test_time_num_step + 1, dtype=np.float32)
58 | smooth_flow_loss_all = np.zeros(args.test_time_num_step + 1, dtype=np.float32)
59 | epe_all = np.zeros(args.test_time_num_step + 1, dtype=np.float32)
60 |
61 | for step in range(args.test_time_num_step):
62 | refined_flow = self(flow)
63 | target_pc_recon = pc_0_orig + refined_flow
64 |
65 | target_recon_loss = chamfer_loss(target_pc_recon, pc_1_orig, corr_conf,
66 | backward_dist_weight=args.backward_dist_weight, mask=None, use_chamfer_cuda=bool(args.use_chamfer_cuda))
67 | loss = args.target_recon_loss_weight * target_recon_loss
68 |
69 | if args.use_smooth_flow and args.smooth_flow_loss_weight > 0:
70 | smooth_flow_loss, _ = smooth_loss(refined_flow, graph, args.nb_neigh_smooth_flow, loss_norm=1, mask=None)
71 | loss = loss + (args.smooth_flow_loss_weight * smooth_flow_loss)
72 | else:
73 | smooth_flow_loss = 0
74 |
75 | # Gradient step
76 | optimizer.zero_grad()
77 | loss.backward()
78 | optimizer.step()
79 |
80 | # Loss evolution
81 | loss_curr = loss.item()
82 | target_recon_loss_curr = target_recon_loss.item()
83 | smooth_flow_loss_curr = smooth_flow_loss.item() if args.use_smooth_flow and args.smooth_flow_loss_weight > 0 else smooth_flow_loss
84 |
85 | # EPE
86 | error = refined_flow - gt_flow_orig
87 | epe_per_point = torch.sqrt(torch.sum(torch.pow(error, 2.0), -1))
88 | epe = epe_per_point.mean()
89 | epe_curr = epe.item()
90 |
91 | if args.test_time_verbose:
92 | print("Refinement step %04d/%04d: loss: %.6f, target_recon_loss: %.6f, smooth_flow_loss: %.6f, epe: %.3f" %
93 | ((step + 1), args.test_time_num_step, loss_curr, target_recon_loss_curr, smooth_flow_loss_curr, epe_curr))
94 |
95 | # aggregate results
96 | target_recon_loss_all[step] = target_recon_loss_curr
97 | smooth_flow_loss_all[step] = smooth_flow_loss_curr
98 | epe_all[step] = epe_curr
99 |
100 | refined_flow = self(flow)
101 |
102 | duration = time.time() - start_time
103 |
104 | # EPE last
105 | error = refined_flow - gt_flow_orig
106 | epe_per_point = torch.sqrt(torch.sum(torch.pow(error, 2.0), -1))
107 | epe = epe_per_point.mean()
108 | epe_curr = epe.item()
109 |
110 | epe_all[-1] = epe_curr
111 |
112 | refine_metrics = {"target_recon_loss_all": target_recon_loss_all, "smooth_flow_loss_all": smooth_flow_loss_all, "epe_all": epe_all}
113 |
114 | return refined_flow, refine_metrics, duration
115 |
--------------------------------------------------------------------------------
/models/scoop.py:
--------------------------------------------------------------------------------
1 | import torch
2 | from tools import ot, reconstruction as R
3 | from models.graph import Graph
4 | from models.gconv import SetConv
5 |
6 |
7 | class SCOOP(torch.nn.Module):
8 | def __init__(self, args):
9 | """
10 | Construct a model that, once trained, estimate the scene flow between
11 | two point clouds.
12 |
13 | Parameters
14 | ----------
15 | args.nb_iter : int
16 | Number of iterations to unroll in the Sinkhorn algorithm.
17 |
18 | """
19 |
20 | super(SCOOP, self).__init__()
21 |
22 | # Hand-chosen parameters. Define the number of channels.
23 | n = 32
24 |
25 | # OT parameters
26 | # Number of unrolled iterations in the Sinkhorn algorithm
27 | self.nb_iter = args.nb_iter
28 | # Mass regularisation
29 | self.gamma = torch.nn.Parameter(torch.zeros(1))
30 | # Entropic regularisation
31 | self.epsilon = torch.nn.Parameter(torch.zeros(1))
32 |
33 | # Architecture parameters
34 | self.nb_neigh_cross_recon = args.nb_neigh_cross_recon
35 |
36 | try:
37 | self.linear_corr_conf = args.linear_corr_conf
38 | except AttributeError:
39 | self.linear_corr_conf = 0
40 |
41 | # Feature extraction
42 | self.feat_conv1 = SetConv(3, n)
43 | self.feat_conv2 = SetConv(n, 2 * n)
44 | self.feat_conv3 = SetConv(2 * n, 4 * n)
45 |
46 | def get_features(self, pcloud, nb_neighbors=32):
47 | """
48 | Compute deep features for each point of the input point cloud. These
49 | features are used to compute the transport cost matrix between two
50 | point clouds.
51 |
52 | Parameters
53 | ----------
54 | pcloud : torch.Tensor
55 | Input point cloud of size B x N x 3
56 | nb_neighbors : int
57 | Number of nearest neighbors for each point.
58 |
59 | Returns
60 | -------
61 | x : torch.Tensor
62 | Deep features for each point. Size B x N x 128
63 | graph : scoop.models.graph.Graph
64 | Graph build on input point cloud containing list of nearest
65 | neighbors (NN) and edge features (relative coordinates with NN).
66 |
67 | """
68 |
69 | graph = Graph.construct_graph(pcloud, nb_neighbors)
70 | x = self.feat_conv1(pcloud, graph)
71 | x = self.feat_conv2(x, graph)
72 | x = self.feat_conv3(x, graph)
73 |
74 | return x, graph
75 |
76 | def get_recon_flow(self, pclouds, feats):
77 | feats_0, feats_1 = feats[0], feats[1]
78 |
79 | # Reconstructed target point cloud
80 | transport_cross, similarity_cross = ot.sinkhorn(
81 | feats_0,
82 | feats_1,
83 | pclouds[0],
84 | pclouds[1],
85 | epsilon=torch.exp(self.epsilon) + 0.03,
86 | gamma=torch.exp(self.gamma),
87 | max_iter=self.nb_iter,
88 | )
89 |
90 | if self.nb_neigh_cross_recon > 0:
91 | source_cross_nn_weight, _, source_cross_nn_idx, _, _, _ = \
92 | R.get_s_t_neighbors(self.nb_neigh_cross_recon, transport_cross, sim_normalization="none", s_only=True)
93 |
94 | # Target point cloud cross reconstruction
95 | cross_weight_sum = source_cross_nn_weight.sum(-1, keepdim=True)
96 | source_cross_nn_weight_normalized = source_cross_nn_weight / (cross_weight_sum + 1e-8)
97 | target_cross_recon = R.reconstruct(pclouds[1], source_cross_nn_idx, source_cross_nn_weight_normalized, self.nb_neigh_cross_recon)
98 |
99 | # Matching probability
100 | cross_nn_sim, _, _, _ = R.get_s_t_topk(similarity_cross, self.nb_neigh_cross_recon, s_only=True, nn_idx=source_cross_nn_idx)
101 | nn_sim_weighted = cross_nn_sim * source_cross_nn_weight_normalized
102 | nn_sim_weighted = torch.sum(nn_sim_weighted, dim=2)
103 | if self.linear_corr_conf:
104 | corr_conf = (nn_sim_weighted + 1) / 2
105 | else:
106 | corr_conf = torch.clamp_min(nn_sim_weighted, 0.0)
107 | else:
108 | row_sum = transport_cross.sum(-1, keepdim=True)
109 | target_cross_recon = (transport_cross @ pclouds[1]) / (row_sum + 1e-8)
110 | corr_conf = None
111 |
112 | # Estimate flow from target cross reconstruction
113 | recon_flow = target_cross_recon - pclouds[0]
114 |
115 | return recon_flow, corr_conf, target_cross_recon
116 |
117 | def forward(self, pclouds):
118 | """
119 | Estimate scene flow between two input point clouds.
120 |
121 | Parameters
122 | ----------
123 | pclouds : (torch.Tensor, torch.Tensor)
124 | List of input point clouds (pc1, pc2). pc1 has size B x N x 3.
125 | pc2 has size B x M x 3.
126 |
127 | Returns
128 | -------
129 | est_flow : torch.Tensor
130 | Estimated scene flow of size B x N x 3.
131 |
132 | """
133 |
134 | # Extract features
135 | feats_0, graph = self.get_features(pclouds[0])
136 | feats_1, _ = self.get_features(pclouds[1])
137 | feats = [feats_0, feats_1]
138 |
139 | # Get reconstruction-based flow
140 | recon_flow, corr_conf, target_cross_recon = self.get_recon_flow(pclouds, feats)
141 |
142 | return recon_flow, corr_conf, target_cross_recon, graph
143 |
--------------------------------------------------------------------------------
/scripts/evaluate_on_ft3d_s.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # evaluate on ft3d_s
4 | python evaluate_scoop.py --dataset_name HPLFlowNet_ft3d --mode test --nb_points 8192 --all_points 0 --all_candidates 0 \
5 | --path2ckpt ./../experiments/ft3d_s_1800_examples/model_e060.tar --backward_dist_weight 1.0 \
6 | --use_test_time_refinement 1 --test_time_num_step 1000 --test_time_update_rate 0.1 --nb_neigh_smooth_flow 16 \
7 | --log_fname log_evaluation_ft3d_s.txt
8 |
--------------------------------------------------------------------------------
/scripts/evaluate_on_kitti_o.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # evaluate on kitti_o
4 | python evaluate_scoop.py --dataset_name FlowNet3D_kitti --mode all --nb_points 2048 --all_points 0 --all_candidates 0 \
5 | --path2ckpt ./../experiments/ft3d_o_1800_examples/model_e100.tar \
6 | --use_test_time_refinement 1 --test_time_num_step 1000 --test_time_update_rate 0.05 \
7 | --log_fname log_evaluation_kitti_o.txt
8 |
--------------------------------------------------------------------------------
/scripts/evaluate_on_kitti_o_all_points.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # evaluate on kitti_o with all the points
4 | python evaluate_scoop.py --dataset_name FlowNet3D_kitti --mode all --nb_points 2048 \
5 | --path2ckpt ./../experiments/ft3d_o_1800_examples/model_e100.tar \
6 | --use_test_time_refinement 1 --test_time_num_step 150 --test_time_update_rate 0.2 \
7 | --log_fname log_evaluation_kitti_o_all_points.txt
8 |
--------------------------------------------------------------------------------
/scripts/evaluate_on_kitti_s.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # evaluate on kitti_s
4 | python evaluate_scoop.py --dataset_name HPLFlowNet_kitti --mode all --nb_points 8192 --all_points 0 --all_candidates 0 \
5 | --path2ckpt ./../experiments/ft3d_s_1800_examples/model_e060.tar --backward_dist_weight 1.0 \
6 | --use_test_time_refinement 1 --test_time_num_step 1000 --test_time_update_rate 0.05 \
7 | --log_fname log_evaluation_kitti_s.txt
--------------------------------------------------------------------------------
/scripts/evaluate_on_kitti_t.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # evaluate on kitti_t
4 | python evaluate_scoop.py --dataset_name FlowNet3D_kitti --mode test --nb_points 2048 --all_points 0 --all_candidates 0 \
5 | --path2ckpt ./../experiments/kitti_v_100_examples/model_e400.tar \
6 | --use_test_time_refinement 1 --test_time_num_step 1000 --test_time_update_rate 0.05 \
7 | --log_fname log_evaluation_kitti_t.txt
8 |
--------------------------------------------------------------------------------
/scripts/evaluate_on_kitti_t_all_points.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # evaluate on kitti_t with all the points
4 | python evaluate_scoop.py --dataset_name FlowNet3D_kitti --mode test --nb_points 2048 \
5 | --path2ckpt ./../experiments/kitti_v_100_examples/model_e400.tar \
6 | --use_test_time_refinement 1 --test_time_num_step 150 --test_time_update_rate 0.2 \
7 | --log_fname log_evaluation_kitti_t_all_points.txt
8 |
--------------------------------------------------------------------------------
/scripts/evaluate_scoop.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | import torch
4 | import argparse
5 | import numpy as np
6 | import time
7 | from tqdm import tqdm
8 |
9 | # add path
10 | project_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
11 | if project_dir not in sys.path:
12 | sys.path.append(project_dir)
13 |
14 | from models.scoop import SCOOP
15 | from models.refiner import Refiner
16 | from datasets.generic import Batch
17 | from tools.seed import seed_everything
18 | from tools.utils import log_string, iterate_in_chunks
19 | from torch.utils.data import DataLoader
20 |
21 |
22 | def compute_flow(scoop, batch, args):
23 | pc_0, pc_1 = batch["sequence"][0], batch["sequence"][1]
24 |
25 | n0 = int(batch["orig_size"][0].cpu().numpy())
26 | n1 = int(batch["orig_size"][1].cpu().numpy())
27 |
28 | with torch.no_grad():
29 | est_flow = torch.zeros([1, n0, 3], dtype=torch.float32, device=pc_0.device)
30 | corr_conf = torch.zeros([1, n0], dtype=torch.float32, device=pc_0.device)
31 |
32 | feats_0, graph = scoop.get_features(pc_0)
33 | feats_1, _ = scoop.get_features(pc_1)
34 |
35 | b, nb_points0, c = feats_0.shape
36 | b, nb_points1, c = feats_1.shape
37 |
38 | pc_0_orig = torch.unsqueeze(torch.reshape(pc_0, (b * nb_points0, 3))[:n0], dim=0)
39 | pc_1_orig = torch.unsqueeze(torch.reshape(pc_1, (b * nb_points1, 3))[:n1], dim=0)
40 | feats_0_orig = torch.unsqueeze(torch.reshape(feats_0, (b * nb_points0, c))[:n0], dim=0)
41 | feats_1_orig = torch.unsqueeze(torch.reshape(feats_1, (b * nb_points1, c))[:n1], dim=0)
42 | idx = np.arange(n0)
43 | for b in iterate_in_chunks(idx, args.nb_points_chunk):
44 | points = pc_0_orig[:, b]
45 | feats = feats_0_orig[:, b]
46 | points_flow, points_conf, _ = scoop.get_recon_flow([points, pc_1_orig], [feats, feats_1_orig])
47 | est_flow[:, b] = points_flow
48 | corr_conf[:, b] = points_conf
49 |
50 | return est_flow, corr_conf, graph
51 |
52 |
53 | def compute_epe_test(est_flow, batch, args):
54 | """
55 | Compute EPE, accuracy and number of outliers.
56 |
57 | Parameters
58 | ----------
59 | est_flow : torch.Tensor
60 | Estimated flow.
61 | batch : scoop.datasets.generic.Batch
62 | Contains ground truth flow and mask.
63 | args : Namespace
64 | Arguments for evaluation.
65 |
66 | Returns
67 | -------
68 | EPE3D : float
69 | End point error.
70 | acc3d_strict : float
71 | Strict accuracy.
72 | acc3d_relax : float
73 | Relax accuracy.
74 | outlier : float
75 | Percentage of outliers.
76 |
77 | """
78 |
79 | # Extract occlusion mask
80 | mask = batch["ground_truth"][0].cpu().numpy()[..., 0]
81 |
82 | # Flow
83 | sf_gt_before_mask = batch["ground_truth"][1].cpu().numpy()
84 | sf_pred_before_mask = est_flow.cpu().numpy()
85 |
86 | # In all_points evaluation mode, take only the original points of the source point cloud
87 | n1 = batch["orig_size"][0].cpu().numpy()
88 | if args.all_points:
89 | assert len(n1) == 1, "If evaluating with all points, the batch size should be equal 1 (got %d)" % len(n1)
90 |
91 | mask = mask.reshape(-1)[:int(n1)]
92 |
93 | sf_gt_before_mask = sf_gt_before_mask.reshape([-1, 3])[:int(n1)]
94 | sf_pred_before_mask = sf_pred_before_mask.reshape([-1, 3])[:int(n1)]
95 |
96 | # Flow
97 | sf_gt = sf_gt_before_mask[mask > 0]
98 | sf_pred = sf_pred_before_mask[mask > 0]
99 |
100 | # EPE
101 | epe3d_per_point = np.linalg.norm(sf_gt - sf_pred, axis=-1)
102 | epe3d = epe3d_per_point.mean()
103 |
104 | #
105 | sf_norm = np.linalg.norm(sf_gt, axis=-1)
106 | relative_err_per_point = epe3d_per_point / (sf_norm + 1e-4)
107 | acc3d_strict_per_point = (np.logical_or(epe3d_per_point < 0.05, relative_err_per_point < 0.05)).astype(np.float32)
108 | acc3d_strict = acc3d_strict_per_point.mean()
109 | acc3d_relax_per_point = (np.logical_or(epe3d_per_point < 0.1, relative_err_per_point < 0.1)).astype(np.float32)
110 | acc3d_relax = acc3d_relax_per_point.mean()
111 | outlier_per_point = (np.logical_or(epe3d_per_point > 0.3, relative_err_per_point > 0.1)).astype(np.float32)
112 | outlier = outlier_per_point.mean()
113 |
114 | return epe3d, acc3d_strict, acc3d_relax, outlier, epe3d_per_point, acc3d_strict_per_point, acc3d_relax_per_point, outlier_per_point
115 |
116 |
117 | def eval_model(scoop, testloader, log_file, log_dir, res_dir, args):
118 | """
119 | Compute performance metrics on test / validation set.
120 |
121 | Parameters
122 | ----------
123 | scoop : scoop.models.SCOOP
124 | SCOOP model to evaluate.
125 | testloader : scoop.datasets.generic.SceneFlowDataset
126 | Dataset loader.
127 | log_file: file
128 | Evaluation log file.
129 | log_dir:
130 | Directory for saving results.
131 | res_dir: srt
132 | Directory for saving point cloud results (active if not None).
133 | args : Namespace
134 | Arguments for evaluation.
135 |
136 | Returns
137 | -------
138 | mean_epe : float
139 | Average EPE on dataset.
140 | mean_outlier : float
141 | Average percentage of outliers.
142 | mean_acc3d_relax : float
143 | Average relaxed accuracy.
144 | mean_acc3d_strict : TYPE
145 | Average strict accuracy.
146 |
147 | """
148 |
149 | # Init.
150 | start_time_eval = time.time()
151 | num_batch = len(testloader)
152 | fname_list = [None] * num_batch
153 | epe_per_scene = np.zeros(num_batch, dtype=np.float32)
154 | acc3d_strict_per_scene = np.zeros(num_batch, dtype=np.float32)
155 | acc3d_relax_per_scene = np.zeros(num_batch, dtype=np.float32)
156 | outlier_per_scene = np.zeros(num_batch, dtype=np.float32)
157 | duration_per_scene = np.zeros(num_batch, dtype=np.float32)
158 | epe_per_point_list = [None] * num_batch
159 | acc3d_strict_per_point_list = [None] * num_batch
160 | acc3d_relax_per_point_list = [None] * num_batch
161 | outlier_per_point_list = [None] * num_batch
162 | running_epe = 0
163 | running_acc3d_strict = 0
164 | running_acc3d_relax = 0
165 | running_outlier = 0
166 |
167 | if args.use_test_time_refinement:
168 | target_recon_loss_refinement = np.zeros([num_batch, args.test_time_num_step + 1], dtype=np.float32)
169 | smooth_flow_loss_refinement = np.zeros([num_batch, args.test_time_num_step + 1], dtype=np.float32)
170 | epe_refinement = np.zeros([num_batch, args.test_time_num_step + 1], dtype=np.float32)
171 | duration_refinement = np.zeros([num_batch], dtype=np.float32)
172 |
173 | save_pc_res = res_dir is not None
174 |
175 | #
176 | scoop = scoop.eval()
177 | device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
178 | for it, batch in enumerate(tqdm(testloader)):
179 |
180 | # Current file name
181 | fname = os.path.split(testloader.dataset.filename_curr)[-1]
182 | fname_list[it] = fname
183 |
184 | # Send data to GPU
185 | batch = batch.to(device, non_blocking=True)
186 |
187 | # Estimate flow
188 | start_time = time.time()
189 | if args.all_candidates:
190 | est_flow, corr_conf, graph = compute_flow(scoop, batch, args)
191 | else:
192 | with torch.no_grad():
193 | est_flow, corr_conf, _, graph = scoop(batch["sequence"])
194 | duration_corr = time.time() - start_time
195 |
196 | duration_total = duration_corr
197 | if args.use_test_time_refinement:
198 | refiner = Refiner(est_flow.shape, est_flow.device)
199 | test_time_optimizer = torch.optim.Adam(refiner.parameters(), lr=args.test_time_update_rate)
200 | refined_flow, refine_metrics_curr, duration_curr = refiner.refine_flow(batch, est_flow.detach(), corr_conf.detach(), test_time_optimizer, args)
201 | est_flow = refined_flow.detach()
202 |
203 | target_recon_loss_refinement[it] = refine_metrics_curr["target_recon_loss_all"]
204 | smooth_flow_loss_refinement[it] = refine_metrics_curr["smooth_flow_loss_all"]
205 | epe_refinement[it] = refine_metrics_curr["epe_all"]
206 | duration_refinement[it] = duration_curr
207 |
208 | duration_total = duration_total + duration_curr
209 |
210 | # Performance metrics
211 | epe3d, acc3d_strict, acc3d_relax, outlier, epe3d_pp, acc3d_strict_pp, acc3d_relax_pp, outlier_pp =\
212 | compute_epe_test(est_flow, batch, args)
213 | epe_per_scene[it] = epe3d
214 | acc3d_strict_per_scene[it] = acc3d_strict
215 | acc3d_relax_per_scene[it] = acc3d_relax
216 | outlier_per_scene[it] = outlier
217 | duration_per_scene[it] = duration_total
218 | epe_per_point_list[it] = epe3d_pp
219 | acc3d_strict_per_point_list[it] = acc3d_strict_pp
220 | acc3d_relax_per_point_list[it] = acc3d_relax_pp
221 | outlier_per_point_list[it] = outlier_pp
222 | running_epe += epe3d
223 | running_outlier += outlier
224 | running_acc3d_relax += acc3d_relax
225 | running_acc3d_strict += acc3d_strict
226 |
227 | # Save point cloud results
228 | if save_pc_res:
229 | n1 = int(batch["orig_size"][0])
230 | n1_save = n1 if args.all_points else int(batch["sequence"][0].shape[1])
231 | pc1_save = batch["sequence"][0].cpu().numpy().reshape([-1, 3])[:n1_save]
232 | est_flow_save = est_flow.cpu().numpy().reshape([-1, 3])[:n1_save]
233 | corr_conf_save = corr_conf.cpu().numpy().reshape([-1])[:n1_save]
234 | mask_save = batch["ground_truth"][0].cpu().numpy().reshape([-1, 1])[:n1_save] == 1
235 | gt_save = batch["ground_truth"][1].cpu().numpy().reshape([-1, 3])[:n1_save]
236 |
237 | n2 = int(batch["orig_size"][1])
238 | n2_save = n2 if args.all_points else int(batch["sequence"][1].shape[1])
239 | pc2_save = batch["sequence"][1].cpu().numpy().reshape([-1, 3])[:n2_save]
240 |
241 | fname_split = fname.split(".")
242 | fname_pc_save = ".".join([fname_split[0] + "_res", fname_split[1]])
243 | path_pc_save = os.path.join(res_dir, fname_pc_save)
244 | np.savez(path_pc_save, pc1=pc1_save, pc2=pc2_save, gt_mask_for_pc1=mask_save, gt_flow_for_pc1=gt_save, est_flow_for_pc1=est_flow_save, corr_conf_for_pc1=corr_conf_save)
245 |
246 | mean_epe = running_epe / num_batch
247 | mean_outlier = running_outlier / num_batch
248 | mean_acc3d_relax = running_acc3d_relax / num_batch
249 | mean_acc3d_strict = running_acc3d_strict / num_batch
250 |
251 | log_string(log_file, "EPE: %.4f, ACC3DS: %.4f, ACC3DR: %.4f, Outlier: %.4f, Dataset Size: %d" %
252 | (mean_epe, mean_acc3d_strict, mean_acc3d_relax, mean_outlier, num_batch))
253 |
254 | duration_eval = time.time() - start_time_eval
255 | log_string(log_file, "Evaluation duration: %.2f minutes (time per example: %.2f seconds)" %
256 | (duration_eval/60, duration_eval/num_batch))
257 |
258 | fnames = np.array(fname_list)
259 | if args.save_metrics:
260 | path_metrics_save = os.path.join(log_dir, args.metrics_fname)
261 | data_for_save = {"fnames": fnames,
262 | "epe_per_scene": epe_per_scene,
263 | "acc3d_strict_per_scene": acc3d_strict_per_scene,
264 | "acc3d_relax_per_scene": acc3d_relax_per_scene,
265 | "outlier_per_scene": outlier_per_scene,
266 | "duration_per_scene": duration_per_scene,
267 | "epe_per_point": epe_per_point_list
268 | }
269 |
270 | if args.use_test_time_refinement:
271 | data_for_save["target_recon_loss_refinement"] = target_recon_loss_refinement
272 | data_for_save["smooth_flow_loss_refinement"] = smooth_flow_loss_refinement
273 | data_for_save["epe_refinement"] = epe_refinement
274 | data_for_save["duration_refinement"] = duration_refinement
275 |
276 | np.savez(path_metrics_save, **data_for_save)
277 |
278 | return mean_epe, mean_outlier, mean_acc3d_relax, mean_acc3d_strict
279 |
280 |
281 | def my_main(args):
282 | """
283 | Entry point of the script.
284 |
285 | Parameters
286 | ----------
287 | args.dataset_name : str
288 | Dataset for evaluation. Either FlowNet3D_kitti or FlowNet3D_ft3d or HPLFlowNet_kitti or HPLFlowNet_ft3d.
289 | args.batch_size: int
290 | Batch size for evaluation.
291 | args.nb_points : int
292 | Number of points in point clouds.
293 | args.path2ckpt : str
294 | Path to saved model.
295 | args.mode : str
296 | Whether to use test set of validation set or all set.
297 |
298 | Raises
299 | ------
300 | ValueError
301 | Unknown dataset.
302 |
303 | """
304 |
305 | # Set seed
306 | seed = seed_everything(seed=42)
307 |
308 | # Path to current file
309 | pathroot = os.path.dirname(__file__)
310 |
311 | # Select dataset
312 | if args.dataset_name.split("_")[0].lower() == "HPLFlowNet".lower():
313 |
314 | # HPLFlowNet version of the datasets
315 | path2data = os.path.join(pathroot, "..", "data", "HPLFlowNet")
316 |
317 | # KITTI
318 | if args.dataset_name.split("_")[1].lower() == "kitti".lower():
319 | path2data = os.path.join(path2data, "KITTI_processed_occ_final")
320 | from datasets.kitti_hplflownet import Kitti
321 |
322 | assert args.mode == "val" or args.mode == "test" or args.mode == "all", "Problem with mode " + args.mode
323 | dataset = Kitti(root_dir=path2data, nb_points=args.nb_points, all_points=args.all_points, mode=args.mode)
324 |
325 | # FlyingThing3D
326 | elif args.dataset_name.split("_")[1].lower() == "ft3d".lower():
327 | path2data = os.path.join(path2data, "FlyingThings3D_subset_processed_35m")
328 | from datasets.flyingthings3d_hplflownet import FT3D
329 |
330 | assert args.mode == "val" or args.mode == "test", "Problem with mode " + args.mode
331 | dataset = FT3D(root_dir=path2data, nb_points=args.nb_points, all_points=args.all_points, mode=args.mode, nb_examples=-1)
332 |
333 | else:
334 | raise ValueError("Unknown dataset " + args.dataset_name)
335 |
336 | elif args.dataset_name.split("_")[0].lower() == "FlowNet3D".lower():
337 |
338 | # FlowNet3D version of the datasets
339 | path2data = os.path.join(pathroot, "..", "data", "FlowNet3D")
340 |
341 | # KITTI
342 | if args.dataset_name.split("_")[1].lower() == "kitti".lower():
343 | path2data = os.path.join(path2data, "kitti_rm_ground")
344 | from datasets.kitti_flownet3d import Kitti
345 |
346 | assert args.mode == "val" or args.mode == "test" or args.mode == "all", "Problem with mode " + args.mode
347 | dataset = Kitti(root_dir=path2data, nb_points=args.nb_points, all_points=args.all_points,
348 | same_v_t_split=True, mode=args.mode)
349 |
350 | # FlyingThing3D
351 | elif args.dataset_name.split("_")[1].lower() == "ft3d".lower():
352 | path2data = os.path.join(path2data, "data_processed_maxcut_35_20k_2k_8192")
353 | from datasets.flyingthings3d_flownet3d import FT3D
354 |
355 | assert args.mode == "val" or args.mode == "test", "Problem with mode " + args.mode
356 | dataset = FT3D(root_dir=path2data, nb_points=args.nb_points, all_points=args.all_points, mode=args.mode, nb_examples=-1)
357 |
358 | else:
359 | raise ValueError("Unknown dataset" + args.dataset_name)
360 |
361 | else:
362 | raise ValueError("Unknown dataset " + args.dataset_name)
363 | print("\n\nDataset: " + path2data + " " + args.mode)
364 |
365 | # Dataloader
366 | testloader = DataLoader(
367 | dataset,
368 | batch_size=args.batch_size,
369 | pin_memory=True,
370 | shuffle=False,
371 | num_workers=args.nb_workers,
372 | collate_fn=Batch,
373 | drop_last=False,
374 | )
375 |
376 | # Load Checkpoint
377 | file = torch.load(args.path2ckpt)
378 |
379 | # Load parameters
380 | saved_args = file["args"]
381 |
382 | # Load model
383 | scoop = SCOOP(saved_args)
384 | device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
385 | scoop = scoop.to(device, non_blocking=True)
386 | scoop.load_state_dict(file["model"])
387 | scoop = scoop.eval()
388 |
389 | # Log file
390 | log_dir = os.path.split(args.path2ckpt)[0]
391 | log_file = open(os.path.join(log_dir, args.log_fname), 'w')
392 | log_string(log_file, 'Evaluation arguments:')
393 | log_file.write(str(args) + '\n')
394 | log_string(log_file, "Seed: %d" % seed)
395 |
396 | # Point cloud results directory
397 | res_dir = None
398 | if args.save_pc_res:
399 | res_dir = os.path.join(log_dir, args.res_folder)
400 | if not os.path.exists(res_dir):
401 | os.mkdir(res_dir)
402 |
403 | # Evaluation
404 | epsilon = 0.03 + torch.exp(scoop.epsilon).item()
405 | gamma = torch.exp(scoop.gamma).item()
406 | power = gamma / (gamma + epsilon)
407 | log_string(log_file, "Epsilon: %.4f, Power: %.4f" % (epsilon, power))
408 | eval_model(scoop, testloader, log_file, log_dir, res_dir, args)
409 |
410 | log_string(log_file, "Finished Evaluation.")
411 | log_file.close()
412 |
413 |
414 | if __name__ == "__main__":
415 |
416 | # Args
417 | parser = argparse.ArgumentParser(description="Evaluate SCOOP.")
418 | parser.add_argument("--dataset_name", type=str, default="FlowNet3D_kitti", help="Dataset. FlowNet3D_kitti or FlowNet3D_FT3D or Either HPLFlowNet_kitti or HPLFlowNet_FT3D.")
419 | parser.add_argument("--batch_size", type=int, default=1, help="Batch size for evaluation.")
420 | parser.add_argument("--mode", type=str, default="test", help="Test or validation or all dataset (options: [val, test, all]).")
421 | parser.add_argument("--use_test_time_refinement", type=int, default=1, help="1: Use test time refinement, 0: Do not use test time refinement.")
422 | parser.add_argument("--test_time_num_step", type=int, default=150, help="1: Number of steps for test time refinement.")
423 | parser.add_argument("--test_time_update_rate", type=float, default=0.2, help="1: Update rate for test time refinement.")
424 | parser.add_argument("--backward_dist_weight", type=float, default=0.0, help="Backward distance weight for target reconstruction loss in test time refinement.")
425 | parser.add_argument("--target_recon_loss_weight", type=float, default=1.0, help="Weight for target reconstruction loss in test time refinement.")
426 | parser.add_argument("--use_smooth_flow", type=int, default=1, help="1: Use self smooth flow loss in test time refinement, 0: Do not use smooth flow loss.")
427 | parser.add_argument("--nb_neigh_smooth_flow", type=int, default=32, help="Number of neighbor points for smooth flow loss in test time refinement.")
428 | parser.add_argument("--smooth_flow_loss_weight", type=float, default=1.0, help="Weight for smooth flow loss in test time refinement. Active if > 0.")
429 | parser.add_argument("--test_time_verbose", type=int, default=0, help="1: Print test time results during optimization, 0: Do not print.")
430 | parser.add_argument("--use_chamfer_cuda", type=int, default=1, help="1: Use chamfer distance cuda implementation in test time refinement, 0: Use chamfer distance pytorch implementation in test time refinement.")
431 | parser.add_argument("--nb_points", type=int, default=2048, help="Maximum number of points in point cloud.")
432 | parser.add_argument("--all_points", type=int, default=1, help="1: use all point in the source point cloud for evaluation in chunks of nb_points, 0: use only nb_points.")
433 | parser.add_argument("--all_candidates", type=int, default=1, help="1: use all points in the target point cloud as candidates concurrently, 0: use chunks of nb_points from the target point cloud each time.")
434 | parser.add_argument("--nb_points_chunk", type=int, default=2048, help="Number of source points chuck for evaluation with all candidate target points.")
435 | parser.add_argument("--nb_workers", type=int, default=0, help="Number of workers for the dataloader.")
436 | parser.add_argument("--path2ckpt", type=str, default="./../pretrained_models/kitti_v_100_examples/model_e400.tar", help="Path to saved checkpoint.")
437 | parser.add_argument("--log_fname", type=str, default="log_evaluation.txt", help="Evaluation log file name.")
438 | parser.add_argument("--save_pc_res", type=int, default=0, help="1: save point cloud results, 0: do not save point cloud results [default: 0]")
439 | parser.add_argument("--res_folder", type=str, default="pc_res", help="Folder name for saving results.")
440 | parser.add_argument("--save_metrics", type=int, default=0, help="1: save evaluation metrics results, 0: do not save evaluation metrics results [default: 0]")
441 | parser.add_argument("--metrics_fname", type=str, default="metrics_results.npz", help="Name for metrics file.")
442 | args = parser.parse_args()
443 |
444 | # Check arguments
445 | if args.all_points:
446 | assert args.batch_size == 1, "For evaluation with all source points, the batch_size should be equal to 1 (got %d)" % args.batch_size
447 |
448 | if args.all_candidates:
449 | assert args.batch_size == 1, "For evaluation with all candidate target points, the batch_size should be equal to 1 (got %d)" % args.batch_size
450 |
451 | if args.save_pc_res:
452 | assert args.batch_size == 1, "For evaluation with saving point cloud results, the batch_size should be equal to 1 (got %d)" % args.batch_size
453 |
454 | # Launch evaluation
455 | my_main(args)
456 |
--------------------------------------------------------------------------------
/scripts/train_on_ft3d_o.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # train on ft3d_o
4 | python train_scoop.py --dataset_name FlowNet3D_ft3d --nb_train_examples 1800 --nb_points 2048 \
5 | --batch_size_train 4 --batch_size_val 10 --nb_epochs 100 --nb_workers 8 \
6 | --backward_dist_weight 0.0 --use_corr_conf 1 --corr_conf_loss_weight 0.1 \
7 | --add_model_suff 1 --save_model_epoch 25 --log_dir ft3d_o_1800_examples
8 |
--------------------------------------------------------------------------------
/scripts/train_on_ft3d_s.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # train on ft3d_s
4 | python train_scoop.py --dataset_name HPLFlowNet_ft3d --nb_train_examples 1800 --nb_val_examples 200 --nb_points 8192 \
5 | --batch_size_train 1 --batch_size_val 1 --nb_epochs 60 --nb_workers 8 \
6 | --backward_dist_weight 1.0 --use_corr_conf 0 \
7 | --add_model_suff 1 --save_model_epoch 15 --log_dir ft3d_s_1800_examples
8 |
--------------------------------------------------------------------------------
/scripts/train_on_kitti_v.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # train on kitti_v
4 | python train_scoop.py --dataset_name FlowNet3D_kitti --nb_points 2048 \
5 | --batch_size_train 4 --batch_size_val 10 --nb_epochs 400 --nb_workers 8 \
6 | --backward_dist_weight 0.0 --use_corr_conf 1 --corr_conf_loss_weight 0.1 \
7 | --add_model_suff 1 --save_model_epoch 100 --log_dir kitti_v_100_examples
8 |
--------------------------------------------------------------------------------
/scripts/train_scoop.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | import time
4 | import torch
5 | import argparse
6 | from tqdm import tqdm
7 | from torch.utils.data import DataLoader
8 | from torch.utils.tensorboard import SummaryWriter
9 |
10 | # add path
11 | project_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
12 | if project_dir not in sys.path:
13 | sys.path.append(project_dir)
14 |
15 | from datasets.generic import Batch
16 | from models.scoop import SCOOP
17 | from tools.seed import seed_everything
18 | from tools.losses import compute_loss_unsupervised
19 | from tools.utils import log_string
20 |
21 |
22 | def compute_epe_train(recon_flow, batch):
23 | """
24 | Compute EPE during training.
25 |
26 | Parameters
27 | ----------
28 | recon_flow: torch.Tensor
29 | Flow from reconstruction of the target point cloud by the source point cloud.
30 | batch : scoop.datasets.generic.Batch
31 | Contains ground truth flow and mask.
32 |
33 | Returns
34 | -------
35 | epe : torch.Tensor
36 | Mean EPE for current batch.
37 |
38 | """
39 |
40 | mask = batch["ground_truth"][0][..., 0]
41 | true_flow = batch["ground_truth"][1]
42 | error = recon_flow - true_flow
43 | error = error[mask > 0]
44 | epe_per_point = torch.sqrt(torch.sum(torch.pow(error, 2.0), -1))
45 | epe = epe_per_point.mean()
46 |
47 | return epe
48 |
49 |
50 | def train(scoop, train_dataloader, val_dataloader, delta, optimizer, scheduler, path2log, args):
51 | """
52 | Train scene flow model.
53 |
54 | Parameters
55 | ----------
56 | scoop : scoop.models.SCOOP
57 | SCOOP model
58 | train_dataloader : scoop.datasets.generic.SceneFlowDataset
59 | Training Dataset loader.
60 | val_dataloader : scoop.datasets.generic.SceneFlowDataset
61 | Validation Dataset loader.
62 | delta : int
63 | Frequency of logs in number of iterations.
64 | optimizer : torch.optim.Optimizer
65 | Optimiser.
66 | scheduler :
67 | Scheduler.
68 | path2log : str
69 | Where to save logs / model.
70 | args : Namespace
71 | Arguments for training.
72 |
73 | """
74 |
75 | # Set seed
76 | seed = seed_everything(seed=42)
77 |
78 | # Log directory
79 | if not os.path.exists(path2log):
80 | os.makedirs(path2log)
81 | writer = SummaryWriter(path2log)
82 |
83 | # Log file
84 | log_file = open(os.path.join(path2log, args.log_fname), 'w')
85 | log_file.write('Training arguments:')
86 | log_file.write(str(args) + '\n')
87 | log_string(log_file, "Seed: %d" % seed)
88 |
89 | # Train
90 | device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
91 | scoop = scoop.to(device, non_blocking=True)
92 |
93 | total_it = 0
94 | epoch_start = 0
95 | for epoch in range(epoch_start, args.nb_epochs):
96 |
97 | # Initialization
98 | running_loss = 0
99 | training_loss_tot = 0
100 | running_target_recon_loss = 0
101 | training_target_recon_loss_tot = 0
102 | if args.use_corr_conf:
103 | running_corr_conf_loss = 0
104 | training_corr_conf_loss_tot = 0
105 | if args.use_smooth_flow:
106 | running_smooth_flow_loss = 0
107 | training_smooth_flow_loss_tot = 0
108 | running_epe = 0
109 | training_epe_tot = 0
110 | epoch_it = 0
111 |
112 | # Train for one epoch
113 | start = time.time()
114 | scoop = scoop.train()
115 | log_string(log_file, "Training epoch %d out of %d" % (epoch + 1, args.nb_epochs))
116 | for it, batch in enumerate(tqdm(train_dataloader)):
117 |
118 | # Send data to GPU
119 | batch = batch.to(device, non_blocking=True)
120 |
121 | # Run model
122 | recon_flow, corr_conf, target_recon, graph = scoop(batch["sequence"])
123 |
124 | # Compute loss
125 | loss, target_recon_loss, corr_conf_loss, smooth_flow_loss =\
126 | compute_loss_unsupervised(recon_flow, corr_conf, target_recon, graph, batch, args)
127 |
128 | # Gradient step
129 | optimizer.zero_grad()
130 | loss.backward()
131 | optimizer.step()
132 |
133 | # Loss evolution
134 | loss_curr = loss.item()
135 | running_loss += loss_curr
136 | training_loss_tot += loss_curr
137 | target_recon_loss_curr = target_recon_loss.item()
138 | running_target_recon_loss += target_recon_loss_curr
139 | training_target_recon_loss_tot += target_recon_loss_curr
140 | if args.use_corr_conf:
141 | corr_conf_loss_curr = corr_conf_loss.item()
142 | running_corr_conf_loss += corr_conf_loss_curr
143 | training_corr_conf_loss_tot += corr_conf_loss_curr
144 | if args.use_smooth_flow:
145 | smooth_flow_loss_curr = smooth_flow_loss.item()
146 | running_smooth_flow_loss += smooth_flow_loss_curr
147 | training_smooth_flow_loss_tot += smooth_flow_loss_curr
148 |
149 | epe_curr = compute_epe_train(recon_flow, batch).item()
150 | running_epe += epe_curr
151 | training_epe_tot += epe_curr
152 |
153 | # Logs
154 | if it % delta == delta - 1:
155 | # Print / save logs
156 | writer.add_scalar("Loss/training_loss", running_loss / delta, total_it)
157 | writer.add_scalar("Loss/training_target_recon_loss", running_target_recon_loss / delta, total_it)
158 | if args.use_corr_conf:
159 | writer.add_scalar("Loss/training_corr_conf_loss", running_corr_conf_loss / delta, total_it)
160 | if args.use_smooth_flow:
161 | writer.add_scalar("Loss/training_smooth_flow_loss", running_smooth_flow_loss / delta, total_it)
162 | writer.add_scalar("Loss/training_epe", running_epe / delta, total_it)
163 | print("Epoch {0:d} - It. {1:d}: loss = {2:e}".format(epoch + 1, total_it, running_loss / delta))
164 | print((time.time() - start) / 60, "minutes")
165 | # Re-init.
166 | running_loss = 0
167 | running_target_recon_loss = 0
168 | if args.use_corr_conf:
169 | running_corr_conf_loss = 0
170 | if args.use_smooth_flow:
171 | running_smooth_flow_loss = 0
172 | running_epe = 0
173 | start = time.time()
174 |
175 | epoch_it += 1
176 | total_it += 1
177 |
178 | # Training loss
179 | training_loss = training_loss_tot / epoch_it
180 | writer.add_scalar("Loss/training_loss", training_loss, total_it)
181 | training_target_recon_loss = training_target_recon_loss_tot / epoch_it
182 | writer.add_scalar("Loss/training_target_recon_loss", training_target_recon_loss, total_it)
183 | if args.use_corr_conf:
184 | training_corr_conf_loss = training_corr_conf_loss_tot / epoch_it
185 | writer.add_scalar("Loss/training_corr_conf_loss", training_corr_conf_loss, total_it)
186 | if args.use_smooth_flow:
187 | training_smooth_flow_loss = training_smooth_flow_loss_tot / epoch_it
188 | writer.add_scalar("Loss/training_smooth_flow_loss", training_smooth_flow_loss, total_it)
189 | training_epe = training_epe_tot / epoch_it
190 | writer.add_scalar("Loss/training_epe", training_epe, total_it)
191 | log_string(log_file, "Training: loss: %.6f, epe: %.3f" % (training_loss, training_epe))
192 |
193 | # Scheduler
194 | scheduler.step()
195 |
196 | # Validation
197 | scoop = scoop.eval()
198 | val_loss_tot = 0
199 | val_target_recon_loss_tot = 0
200 | if args.use_corr_conf:
201 | val_corr_conf_loss_tot = 0
202 | if args.use_smooth_flow:
203 | val_smooth_flow_loss_tot = 0
204 | val_epe_tot = 0
205 | val_it = 0
206 | with torch.no_grad():
207 | for it, batch in enumerate(tqdm(val_dataloader)):
208 |
209 | # Send data to GPU
210 | batch = batch.to(device, non_blocking=True)
211 |
212 | # Run model
213 | recon_flow, corr_conf, target_recon, graph = scoop(batch["sequence"])
214 |
215 | # Compute loss
216 | loss, target_recon_loss, corr_conf_loss, smooth_flow_loss =\
217 | compute_loss_unsupervised(recon_flow, corr_conf, target_recon, graph, batch, args)
218 |
219 | # Validation loss
220 | val_loss_tot += loss.item()
221 | val_target_recon_loss_tot += target_recon_loss.item()
222 | if args.use_corr_conf:
223 | val_corr_conf_loss_tot += corr_conf_loss.item()
224 | if args.use_smooth_flow:
225 | val_smooth_flow_loss_tot += smooth_flow_loss.item()
226 | val_epe_tot += compute_epe_train(recon_flow, batch).item()
227 | val_it += 1
228 |
229 | val_loss = val_loss_tot / val_it
230 | writer.add_scalar("Loss/validation_loss", val_loss, total_it)
231 | val_target_recon_loss = val_target_recon_loss_tot / val_it
232 | writer.add_scalar("Loss/validation_target_recon_loss", val_target_recon_loss, total_it)
233 | if args.use_corr_conf:
234 | val_corr_conf_loss = val_corr_conf_loss_tot / val_it
235 | writer.add_scalar("Loss/validation_corr_conf_loss", val_corr_conf_loss, total_it)
236 | if args.use_smooth_flow:
237 | val_smooth_flow_loss = val_smooth_flow_loss_tot / val_it
238 | writer.add_scalar("Loss/validation_smooth_flow_loss", val_smooth_flow_loss, total_it)
239 | val_epe = val_epe_tot / val_it
240 | writer.add_scalar("Loss/validation_epe", val_epe, total_it)
241 | log_string(log_file, "Validation: loss: %.6f, epe: %.3f" % (val_loss, val_epe))
242 |
243 | # Save model after each epoch
244 | state = {
245 | "args": args,
246 | "model": scoop.state_dict(),
247 | "optimizer": optimizer.state_dict(),
248 | "scheduler": scheduler.state_dict(),
249 | }
250 | if epoch == 0 or (epoch + 1) % args.save_model_epoch == 0:
251 | suff = args.add_model_suff and "_e%03d" % (epoch + 1) or ""
252 | torch.save(state, os.path.join(path2log, "model%s.tar" % suff))
253 |
254 | log_string(log_file, "Finished Training.")
255 | log_file.close()
256 |
257 | return None
258 |
259 |
260 | def my_main(args):
261 | """
262 | Entry point of the script.
263 |
264 | Parameters
265 | ----------
266 | args.dataset_name : str
267 | Dataset for training. Either FlowNet3D_kitti or FlowNet3D_ft3d or HPLFlowNet_kitti or HPLFlowNet_ft3d.
268 | args.nb_iter : int
269 | Number of unrolled iteration of Sinkhorn algorithm in SCOOP.
270 | args.batch_size_train : int
271 | Batch size fot training dataset.
272 | args.batch_size_val : int
273 | Batch size fot validation dataset.
274 | args.nb_points : int
275 | Number of points in point clouds.
276 | args.nb_epochs : int
277 | Number of epochs.
278 | args.nb_workers: int
279 | Number of workers for the dataloader.
280 | args.log_dir:
281 | Logging directory.
282 |
283 | Raises
284 | ------
285 | ValueError
286 | If dataset_name is an unknown dataset.
287 |
288 | """
289 |
290 | # Path to current file
291 | pathroot = os.path.dirname(__file__)
292 |
293 | # Select dataset
294 | if args.dataset_name.split("_")[0].lower() == "HPLFlowNet".lower():
295 | # HPLFlowNet version of the datasets
296 | path2data = os.path.join(pathroot, "..", "data", "HPLFlowNet")
297 |
298 | # KITTI
299 | if args.dataset_name.split("_")[1].lower() == "kitti".lower():
300 | path2data = os.path.join(path2data, "KITTI_processed_occ_final")
301 | from datasets.kitti_hplflownet import Kitti
302 |
303 | # datasets
304 | train_dataset = Kitti(root_dir=path2data, nb_points=args.nb_points, all_points=False, mode="train")
305 | val_dataset = Kitti(root_dir=path2data, nb_points=args.nb_points, all_points=False, mode="val")
306 |
307 | # learning rate schedule
308 | lr_lambda = lambda epoch: 1.0 if epoch < 50 else 1.0
309 |
310 | # FlyingThing3D
311 | elif args.dataset_name.split("_")[1].lower() == "ft3d".lower():
312 | path2data = os.path.join(path2data, "FlyingThings3D_subset_processed_35m")
313 | from datasets.flyingthings3d_hplflownet import FT3D
314 |
315 | # datasets
316 | train_dataset = FT3D(root_dir=path2data, nb_points=args.nb_points, all_points=False, mode="train", nb_examples=args.nb_train_examples)
317 | val_dataset = FT3D(root_dir=path2data, nb_points=args.nb_points, all_points=False, mode="val", nb_examples=args.nb_val_examples)
318 |
319 | # learning rate schedule
320 | lr_lambda = lambda epoch: 1.0 if epoch < 50 else 0.1
321 |
322 | else:
323 | raise ValueError("Unknown dataset " + args.dataset_name)
324 |
325 | elif args.dataset_name.split("_")[0].lower() == "FlowNet3D".lower():
326 | # FlowNet3D version of the datasets
327 | path2data = os.path.join(pathroot, "..", "data", "FlowNet3D")
328 |
329 | # KITTI
330 | if args.dataset_name.split("_")[1].lower() == "kitti".lower():
331 | path2data = os.path.join(path2data, "kitti_rm_ground")
332 | from datasets.kitti_flownet3d import Kitti
333 |
334 | # datasets
335 | train_dataset = Kitti(root_dir=path2data, nb_points=args.nb_points, all_points=False, same_v_t_split=args.same_val_test_kitti, mode="train")
336 | val_dataset = Kitti(root_dir=path2data, nb_points=args.nb_points, all_points=False, same_v_t_split=args.same_val_test_kitti, mode="val")
337 |
338 | # learning rate schedule
339 | lr_lambda = lambda epoch: 1.0 if epoch < 340 else 0.1
340 |
341 | # FlyingThing3D
342 | elif args.dataset_name.split("_")[1].lower() == "ft3d".lower():
343 | path2data = os.path.join(path2data, "data_processed_maxcut_35_20k_2k_8192")
344 | from datasets.flyingthings3d_flownet3d import FT3D
345 |
346 | # datasets
347 | train_dataset = FT3D(root_dir=path2data, nb_points=args.nb_points, all_points=False, mode="train", nb_examples=args.nb_train_examples)
348 | val_dataset = FT3D(root_dir=path2data, nb_points=args.nb_points, all_points=False, mode="val", nb_examples=args.nb_val_examples)
349 |
350 | # learning rate schedule
351 | lr_lambda = lambda epoch: 1.0 if epoch < 340 else 1.0
352 |
353 | else:
354 | raise ValueError("Unknown dataset" + args.dataset_name)
355 |
356 | else:
357 | raise ValueError("Invalid dataset name: " + args.dataset_name)
358 |
359 | # Training data
360 | train_dataloader = DataLoader(
361 | train_dataset,
362 | batch_size=args.batch_size_train,
363 | pin_memory=True,
364 | shuffle=True,
365 | num_workers=args.nb_workers,
366 | collate_fn=Batch,
367 | drop_last=True,
368 | )
369 |
370 | # Validation data
371 | val_dataloader = DataLoader(
372 | val_dataset,
373 | batch_size=args.batch_size_val,
374 | pin_memory=True,
375 | shuffle=False,
376 | num_workers=args.nb_workers,
377 | collate_fn=Batch,
378 | drop_last=False,
379 | )
380 |
381 | # Model
382 | scoop = SCOOP(args)
383 |
384 | # Optimizer
385 | optimizer = torch.optim.Adam(scoop.parameters(), lr=args.learning_rate)
386 |
387 | # Scheduler
388 | scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda)
389 |
390 | # Log directory
391 | path2log = os.path.join(pathroot, "..", "experiments", args.log_dir)
392 |
393 |
394 | # Train
395 | print("Training started. Logs in " + path2log)
396 | train(scoop, train_dataloader, val_dataloader, 500, optimizer, scheduler, path2log, args)
397 |
398 | return None
399 |
400 |
401 | if __name__ == "__main__":
402 |
403 | # Args
404 | parser = argparse.ArgumentParser(description="Train SCOOP.")
405 | parser.add_argument("--dataset_name", type=str, default="FlowNet3D_kitti", help="Dataset name for train and validation. Either FlowNet3D_kitti or FlowNet3D_ft3d or HPLFlowNet_kitti or HPLFlowNet_ft3d.")
406 | parser.add_argument("--nb_train_examples", type=int, default=-1, help="Number of examples for the training dataset. active if > 0.")
407 | parser.add_argument("--nb_val_examples", type=int, default=-1, help="Number of examples for the validation dataset. active if > 0.")
408 | parser.add_argument("--same_val_test_kitti", type=int, default=1, help="1: Use the same validation and test split for KITTI dataset, 0: Do not use the same validation and test split.")
409 | parser.add_argument("--batch_size_train", type=int, default=4, help="Batch size fot training dataset.")
410 | parser.add_argument("--batch_size_val", type=int, default=10, help="Batch size for validation dataset.")
411 | parser.add_argument("--nb_epochs", type=int, default=400, help="Number of epochs.")
412 | parser.add_argument("--learning_rate", type=float, default=0.001, help="Learning rate.")
413 | parser.add_argument("--use_corr_conf", type=int, default=1, help="1: Use correspondence confidence for training, 0: Do not use correspondence confidence.")
414 | parser.add_argument("--linear_corr_conf", type=int, default=0, help="1: Use linear normalization for correspondence confidence, 0: Use ReLU normalization.")
415 | parser.add_argument("--corr_conf_loss_weight", type=float, default=0.1, help="Weight for correspondence confidence loss.")
416 | parser.add_argument("--nb_points", type=int, default=2048, help="Maximum number of points in point cloud.")
417 | parser.add_argument("--nb_iter", type=int, default=1, help="Number of unrolled iterations of the Sinkhorn algorithm.")
418 | parser.add_argument("--nb_neigh_cross_recon", type=int, default=64, help="Number of neighbor points for cross reconstruction. Active if > 0")
419 | parser.add_argument("--use_smooth_flow", type=int, default=1, help="1: Use smooth flow loss, 0: Do not use smooth flow loss.")
420 | parser.add_argument("--nb_neigh_smooth_flow", type=int, default=32, help="Number of neighbor points for smooth flow loss.")
421 | parser.add_argument("--smooth_flow_loss_weight", type=float, default=10.0, help="Weight for smooth flow loss. Active if > 0.")
422 | parser.add_argument("--backward_dist_weight", type=float, default=0.0, help="Backward distance weight in Chamfer Distance loss.")
423 | parser.add_argument("--nb_workers", type=int, default=0, help="Number of workers for the dataloader.")
424 | parser.add_argument("--log_dir", type=str, default="log_scoop", help="Logging directory.")
425 | parser.add_argument("--log_fname", type=str, default="log_train.txt", help="Evaluation log file name.")
426 | parser.add_argument("--save_model_epoch", type=int, default=1, help="Number of epochs difference for saving the model.")
427 | parser.add_argument("--add_model_suff", type=int, default=0, help="1: Add suffix to model name, 0: do not add suffix.")
428 | args = parser.parse_args()
429 |
430 | args.use_corr_conf = bool(args.use_corr_conf)
431 | args.use_smooth_flow = bool(args.use_smooth_flow)
432 | args.add_model_suff = bool(args.add_model_suff)
433 |
434 | # Check arguments
435 | if args.use_smooth_flow:
436 | assert args.nb_neigh_smooth_flow > 0, "If use_smooth_flow is on, nb_neigh_smooth_flow should be > 0 (got %d)." % args.nb_neigh_smooth_flow
437 | assert args.smooth_flow_loss_weight >= 0, "If use_smooth_flow is on, smooth_flow_loss_weight should be >= 0 (got %f)." % args.smooth_flow_loss_weight
438 |
439 | # Launch training
440 | my_main(args)
441 |
--------------------------------------------------------------------------------
/scripts/visualize_scoop.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | import argparse
4 |
5 | # add path
6 | project_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
7 | if project_dir not in sys.path:
8 | sys.path.append(project_dir)
9 |
10 | from tools.utils import create_dir, load_data
11 | from tools.vis_utils import plot_pc_list, plot_pcs, save_pc_plot
12 |
13 | COLOR_RED = (1, 0, 0)
14 | COLOR_GREEN = (0, 1, 0)
15 | COLOR_BLUE = (0, 0, 1)
16 | COLOR_PURPLE = (127/255., 0, 1)
17 |
18 |
19 | def vis_result(res_dir, res_srt, view_dict, save_dir, save_plot=True, show_plot=False, size=(1600, 800)):
20 | res_path = os.path.join(res_dir, "%s_res.npz" % res_srt)
21 | pc1, pc2, gt_mask, gt_flow, est_flow, corr_conf, pc1_warped_gt, pc1_warped_est = load_data(res_path)
22 |
23 | # pc1, pc2
24 | pc_list = [pc1, pc2]
25 | color_list = [COLOR_RED, COLOR_GREEN]
26 | plot_pc_list(pc_list, color_list, view_dict=view_dict, get_view=False, fig=None, size=size)
27 |
28 | save_path = os.path.join(save_dir, "%s_01_pc1_pc2.png" % res_srt)
29 | save_pc_plot(save_path, save=save_plot, show=show_plot)
30 |
31 | # pc1, pc1_warped_est
32 | pc_list = [pc1, pc1_warped_est]
33 | color_list = [COLOR_RED, COLOR_BLUE]
34 | plot_pc_list(pc_list, color_list, view_dict=view_dict, get_view=False, fig=None, size=size)
35 |
36 | save_path = os.path.join(save_dir, "%s_02_pc1_pc1_warped_est.png" % res_srt)
37 | save_pc_plot(save_path, save=save_plot, show=show_plot)
38 |
39 | # pc2, pc1_warped_est
40 | pc_list = [pc2, pc1_warped_est]
41 | color_list = [COLOR_GREEN, COLOR_BLUE]
42 | plot_pc_list(pc_list, color_list, view_dict=view_dict, get_view=False, fig=None, size=size)
43 |
44 | save_path = os.path.join(save_dir, "%s_03_pc2_pc1_warped_est.png" % res_srt)
45 | save_pc_plot(save_path, save=save_plot, show=show_plot)
46 |
47 | # pc1, pc2, pc1_warped_est
48 | pc_list = [pc1, pc2, pc1_warped_est]
49 | color_list = [COLOR_RED, COLOR_GREEN, COLOR_BLUE]
50 | plot_pc_list(pc_list, color_list, view_dict=view_dict, get_view=False, fig=None, size=size)
51 |
52 | save_path = os.path.join(save_dir, "%s_04_pc1_pc2_pc1_warped_est.png" % res_srt)
53 | save_pc_plot(save_path, save=save_plot, show=show_plot)
54 |
55 | # pc1, pc1_warped_gt
56 | pc_list = [pc1, pc1_warped_gt]
57 | color_list = [COLOR_RED, COLOR_PURPLE]
58 | plot_pc_list(pc_list, color_list, view_dict=view_dict, get_view=False, fig=None, size=size)
59 |
60 | save_path = os.path.join(save_dir, "%s_05_pc1_pc1_warped_gt.png" % res_srt)
61 | save_pc_plot(save_path, save=save_plot, show=show_plot)
62 |
63 | # pc2, pc1_warped_gt
64 | pc_list = [pc2, pc1_warped_gt]
65 | color_list = [COLOR_GREEN, COLOR_PURPLE]
66 | plot_pc_list(pc_list, color_list, view_dict=view_dict, get_view=False, fig=None, size=size)
67 |
68 | save_path = os.path.join(save_dir, "%s_06_pc2_pc1_warped_gt.png" % res_srt)
69 | save_pc_plot(save_path, save=save_plot, show=show_plot)
70 |
71 | # pc1, pc2, pc1_warped_gt
72 | pc_list = [pc1, pc2, pc1_warped_gt]
73 | color_list = [COLOR_RED, COLOR_GREEN, COLOR_PURPLE]
74 | plot_pc_list(pc_list, color_list, view_dict=view_dict, get_view=False, fig=None, size=size)
75 |
76 | save_path = os.path.join(save_dir, "%s_07_pc1_pc2_pc1_warped_gt.png" % res_srt)
77 | save_pc_plot(save_path, save=save_plot, show=show_plot)
78 |
79 | # pc1, est_flow (quiver), pc2
80 | color_dict = {"pc1_c": COLOR_RED, "pc2_c": COLOR_GREEN, "flow_c": COLOR_BLUE}
81 | plot_pcs(pc1, pc2, flow=est_flow, color_dict=color_dict, view_dict=view_dict, get_view=False, size=size)
82 |
83 | save_path = os.path.join(save_dir, "%s_08_pc1_flow_est_pc2.png" % res_srt)
84 | save_pc_plot(save_path, save=save_plot, show=show_plot)
85 |
86 | # pc1, gt_flow (quiver), pc2
87 | color_dict = {"pc1_c": COLOR_RED, "pc2_c": COLOR_GREEN, "flow_c": COLOR_PURPLE}
88 | plot_pcs(pc1, pc2, flow=gt_flow, color_dict=color_dict, view_dict=view_dict, get_view=False, size=size)
89 |
90 | save_path = os.path.join(save_dir, "%s_09_pc1_flow_gt_pc2.png" % res_srt)
91 | save_pc_plot(save_path, save=save_plot, show=show_plot)
92 |
93 |
94 | if __name__ == "__main__":
95 | # Args
96 | parser = argparse.ArgumentParser(description="Visualize SCOOP.")
97 | parser.add_argument("--res_dir", type=str, default="./../pretrained_models/kitti_v_100_examples/pc_res", help="Point cloud results directory.")
98 | parser.add_argument("--res_idx", type=int, default=1, help="Index of the result to visualize.")
99 | args = parser.parse_args()
100 |
101 | view_dict = {'azimuth': 21.245700205624324, 'elevation': 99.80932242859983, 'distance': 13.089033739603677,
102 | 'focalpoint': [0.63038374, -1.79234603, 10.63751715], 'roll': 0.7910293664830116}
103 |
104 | save_dir = create_dir(os.path.join(args.res_dir, "vis"))
105 | vis_result(args.res_dir, "%06d" % args.res_idx, view_dict, save_dir, save_plot=True, show_plot=False, size=(1600, 800))
106 |
--------------------------------------------------------------------------------
/tools/losses.py:
--------------------------------------------------------------------------------
1 | """ Loss functions. """
2 | import os
3 | import sys
4 | import torch
5 | import numpy as np
6 |
7 | # add path
8 | project_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
9 | if project_dir not in sys.path:
10 | sys.path.append(project_dir)
11 |
12 | try:
13 | from auxiliary.ChamferDistancePytorch.chamfer3D import dist_chamfer_3D
14 | chamfer_dist_3d_cu = dist_chamfer_3D.chamfer_3DDist()
15 | except:
16 | print("Could not load compiled 3D CUDA chamfer distance")
17 |
18 | from tools.utils import iterate_in_chunks
19 |
20 |
21 | def compute_loss_unsupervised(recon_flow, corr_conf, target_pc_recon, graph, batch, args):
22 | """
23 | Compute unsupervised training loss.
24 |
25 | Parameters
26 | ----------
27 | recon_flow: torch.Tensor
28 | Flow from reconstruction of the target point cloud by the source point cloud.
29 | corr_conf: torch.Tensor
30 | Correspondence confidence.
31 | target_pc_recon: torch.Tensor
32 | Cross reconstructed target point cloud.
33 | graph: scoop.models.graph.Graph
34 | Nearest neighbor graph for the source point cloud.
35 | batch: scoop.datasets.generic.Batch
36 | Contains ground truth flow and mask.
37 | args: dictionary.
38 | Arguments for loss terms.
39 |
40 | Returns
41 | -------
42 | loss : torch.Tensor
43 | Training loss for current batch.
44 |
45 | """
46 | mask = None
47 |
48 | if args.use_corr_conf:
49 | point_weight = corr_conf
50 | else:
51 | point_weight = None
52 |
53 | target_pc_input = batch["sequence"][1]
54 | target_recon_loss = chamfer_loss(target_pc_recon, target_pc_input, point_weight, args.backward_dist_weight, mask)
55 |
56 | loss = target_recon_loss
57 |
58 | if args.use_corr_conf and args.corr_conf_loss_weight > 0:
59 | if mask is not None:
60 | corr_conf_masked = corr_conf[mask > 0]
61 | else:
62 | corr_conf_masked = corr_conf
63 |
64 | corr_conf_loss = 1 - torch.mean(corr_conf_masked)
65 | loss = loss + (args.corr_conf_loss_weight * corr_conf_loss)
66 | else:
67 | corr_conf_loss = 0
68 |
69 | if args.use_smooth_flow and args.smooth_flow_loss_weight > 0:
70 | smooth_flow_loss, _ = smooth_loss(recon_flow, graph, args.nb_neigh_smooth_flow, loss_norm=1, mask=mask)
71 | loss = loss + (args.smooth_flow_loss_weight * smooth_flow_loss)
72 | else:
73 | smooth_flow_loss = 0
74 |
75 | return loss, target_recon_loss, corr_conf_loss, smooth_flow_loss
76 |
77 |
78 | def chamfer_dist_3d_pt(pc1, pc2, backward_dist_weight=0.0, chunk_size=2048):
79 | """
80 | Compute Chamfer Distance between two point clouds.
81 | Input:
82 | pc1: (b, n, 3) torch.Tensor, first point cloud xyz coordinates.
83 | pc2: (b, m, 3) torch.Tensor, second point cloud xyz coordinates.
84 | backward_dist_weight: float, weight for backward distance
85 | chunk_size: int, chunk size for distance computation.
86 |
87 | Output:
88 | dist1: (b, n) torch.Tensor float32, for each point in pc1, the distance to the closest point in pc2.
89 | dist2: (b, m) torch.Tensor float32, for each point in pc2, the distance to the closest point in pc1.
90 | idx1: (b, n) torch.Tensor int32, for each point in pc1, the index of the closest point in pc2 (values are in the range [0, ..., m-1]).
91 | idx2: (b, m) torch.Tensor int32, for each point in pc2, the index of the closest point in pc1 (values are in the range [0, ..., n-1]).
92 | """
93 |
94 | b = pc1.shape[0]
95 | n = pc1.shape[1]
96 | m = pc2.shape[1]
97 | device = pc1.device
98 |
99 | dist1 = torch.zeros([b, n], dtype=torch.float32, device=device)
100 | idx1 = torch.zeros([b, n], dtype=torch.int32, device=device)
101 |
102 | rng1 = np.arange(n)
103 | for chunk in iterate_in_chunks(rng1, chunk_size):
104 | pc1_curr = torch.unsqueeze(pc1[:, chunk], dim=2).repeat(1, 1, m, 1)
105 | pc2_curr = torch.unsqueeze(pc2, dim=1).repeat(1, len(chunk), 1, 1)
106 | diff = pc1_curr - pc2_curr # shape (b, cs, m, 3)
107 | dist = torch.sum(diff ** 2, dim=-1) # shape (b, cs, m)
108 |
109 | min1 = torch.min(dist, dim=2)
110 | dist1_curr = min1.values
111 | idx1_curr = min1.indices.type(torch.IntTensor)
112 | idx1_curr = idx1_curr.to(dist.device)
113 |
114 | dist1[:, chunk] = dist1_curr
115 | idx1[:, chunk] = idx1_curr
116 |
117 | if backward_dist_weight == 0.0:
118 | dist2 = None
119 | idx2 = None
120 | else:
121 | dist2 = torch.zeros([b, m], dtype=torch.float32, device=device)
122 | idx2 = torch.zeros([b, m], dtype=torch.int32, device=device)
123 |
124 | rng2 = np.arange(m)
125 | for chunk in iterate_in_chunks(rng2, chunk_size):
126 | pc1_curr = torch.unsqueeze(pc1, dim=2).repeat(1, 1, len(chunk), 1)
127 | pc2_curr = torch.unsqueeze(pc2[:, chunk], dim=1).repeat(1, n, 1, 1)
128 | diff = pc1_curr - pc2_curr # shape (b, n, cs, 3)
129 | dist = torch.sum(diff ** 2, dim=-1) # shape (b, n, cs)
130 |
131 | min2 = torch.min(dist, dim=1)
132 | dist2_curr = min2.values
133 | idx2_curr = min2.indices.type(torch.IntTensor)
134 | idx2_curr = idx2_curr.to(dist.device)
135 |
136 | dist2[:, chunk] = dist2_curr
137 | idx2[:, chunk] = idx2_curr
138 |
139 | return dist1, dist2, idx1, idx2
140 |
141 |
142 | def chamfer_loss(pc1, pc2, point_weight=None, backward_dist_weight=1.0, mask=None, use_chamfer_cuda=True):
143 | if not pc1.is_cuda:
144 | pc1 = pc1.cuda()
145 |
146 | if not pc2.is_cuda:
147 | pc2 = pc2.cuda()
148 |
149 | if use_chamfer_cuda:
150 | dist1, dist2, idx1, idx2 = chamfer_dist_3d_cu(pc1, pc2)
151 | else:
152 | dist1, dist2, idx1, idx2 = chamfer_dist_3d_pt(pc1, pc2, backward_dist_weight)
153 |
154 | if point_weight is not None:
155 | dist1_weighted = dist1 * point_weight
156 | else:
157 | dist1_weighted = dist1
158 |
159 | if mask is not None:
160 | dist1_masked = dist1_weighted[mask > 0]
161 | dist1_mean = torch.mean(dist1_masked)
162 | else:
163 | dist1_mean = torch.mean(dist1_weighted)
164 |
165 | if backward_dist_weight == 1.0:
166 | loss = dist1_mean + torch.mean(dist2)
167 | elif backward_dist_weight == 0.0:
168 | loss = dist1_mean
169 | else:
170 | loss = dist1_mean + backward_dist_weight * torch.mean(dist2)
171 |
172 | return loss
173 |
174 |
175 | def smooth_loss(est_flow, graph, nb_neigh, loss_norm=1, mask=None):
176 | b, n, c = est_flow.shape
177 | est_flow_neigh = est_flow.reshape(b * n, c)
178 | est_flow_neigh = est_flow_neigh[graph.edges]
179 | est_flow_neigh = est_flow_neigh.view(b, n, graph.k_neighbors, c)
180 | est_flow_neigh = est_flow_neigh[:, :, 1:(nb_neigh + 1)]
181 | flow_diff = (est_flow.unsqueeze(2) - est_flow_neigh).norm(p=loss_norm, dim=-1)
182 |
183 | if mask is not None:
184 | mask_neigh = mask.reshape(b * n)
185 | mask_neigh = mask_neigh[graph.edges]
186 | mask_neigh = mask_neigh.view(b, n, graph.k_neighbors)
187 | mask_neigh = mask_neigh[:, :, 1:(nb_neigh + 1)]
188 | mask_neigh_sum = mask_neigh.sum(dim=-1)
189 |
190 | flow_diff_masked = flow_diff * mask_neigh
191 | flow_diff_masked_sum = flow_diff_masked.sum(dim=-1)
192 | smooth_flow_per_point = flow_diff_masked_sum / (mask_neigh_sum + 1e-8)
193 | smooth_flow_per_point = smooth_flow_per_point[mask > 0]
194 | else:
195 | smooth_flow_per_point = flow_diff.mean(dim=-1)
196 |
197 | smooth_flow_loss = smooth_flow_per_point.mean()
198 |
199 | return smooth_flow_loss, smooth_flow_per_point
200 |
--------------------------------------------------------------------------------
/tools/ot.py:
--------------------------------------------------------------------------------
1 | import torch
2 |
3 |
4 | def sinkhorn(feature1, feature2, pcloud1, pcloud2, epsilon, gamma, max_iter):
5 | """
6 | Sinkhorn algorithm
7 |
8 | Parameters
9 | ----------
10 | feature1 : torch.Tensor
11 | Feature for points cloud 1. Used to computed transport cost.
12 | Size B x N x C.
13 | feature2 : torch.Tensor
14 | Feature for points cloud 2. Used to computed transport cost.
15 | Size B x M x C.
16 | pcloud1 : torch.Tensor
17 | Point cloud 1. Size B x N x 3.
18 | pcloud2 : torch.Tensor
19 | Point cloud 2. Size B x M x 3.
20 | epsilon : torch.Tensor
21 | Entropic regularisation. Scalar.
22 | gamma : torch.Tensor
23 | Mass regularisation. Scalar.
24 | max_iter : int
25 | Number of unrolled iteration of the Sinkhorn algorithm.
26 |
27 | Returns
28 | -------
29 | torch.Tensor
30 | Transport plan between point cloud 1 and 2. Size B x N x M.
31 | """
32 |
33 | # Squared l2 distance between points points of both point clouds
34 | distance_matrix = torch.sum(pcloud1 ** 2, -1, keepdim=True)
35 | distance_matrix = distance_matrix + torch.sum(
36 | pcloud2 ** 2, -1, keepdim=True
37 | ).transpose(1, 2)
38 | distance_matrix = distance_matrix - 2 * torch.bmm(pcloud1, pcloud2.transpose(1, 2))
39 | # Force transport to be zero for points further than 10 m apart
40 | support = (distance_matrix < 10 ** 2).float()
41 |
42 | # Transport cost matrix
43 | feature1 = feature1 / torch.sqrt(torch.sum(feature1 ** 2, -1, keepdim=True) + 1e-8)
44 | feature2 = feature2 / torch.sqrt(torch.sum(feature2 ** 2, -1, keepdim=True) + 1e-8)
45 | S = torch.bmm(feature1, feature2.transpose(1, 2))
46 | C = 1.0 - S
47 |
48 | # Entropic regularisation
49 | K = torch.exp(-C / epsilon) * support
50 |
51 | # Early return if no iterations
52 | if max_iter == 0:
53 | return K, S
54 |
55 | # Init. of Sinkhorn algorithm
56 | power = gamma / (gamma + epsilon)
57 | a = (
58 | torch.ones(
59 | (K.shape[0], K.shape[1], 1), device=feature1.device, dtype=feature1.dtype
60 | )
61 | / K.shape[1]
62 | )
63 | prob1 = (
64 | torch.ones(
65 | (K.shape[0], K.shape[1], 1), device=feature1.device, dtype=feature1.dtype
66 | )
67 | / K.shape[1]
68 | )
69 | prob2 = (
70 | torch.ones(
71 | (K.shape[0], K.shape[2], 1), device=feature2.device, dtype=feature2.dtype
72 | )
73 | / K.shape[2]
74 | )
75 |
76 | # Sinkhorn algorithm
77 | for _ in range(max_iter):
78 | # Update b
79 | KTa = torch.bmm(K.transpose(1, 2), a)
80 | b = torch.pow(prob2 / (KTa + 1e-8), power)
81 | # Update a
82 | Kb = torch.bmm(K, b)
83 | a = torch.pow(prob1 / (Kb + 1e-8), power)
84 |
85 | # Transportation map
86 | T = torch.mul(torch.mul(a, K), b.transpose(1, 2))
87 |
88 | return T, S
89 |
--------------------------------------------------------------------------------
/tools/reconstruction.py:
--------------------------------------------------------------------------------
1 | import torch
2 | import torch.nn.functional as F
3 |
4 |
5 | def get_support_matrix(pcloud1, pcloud2, dist_thresh=10):
6 | """
7 | Compute support matrix for two point clouds.
8 | The matrix indicates if the distance between two points is less than a threshold.
9 |
10 | Parameters
11 | ----------
12 | pcloud1 : torch.Tensor
13 | Point cloud 1. Size B x N x 3.
14 | pcloud2 : torch.Tensor
15 | Point cloud 2. Size B x M x 3.
16 | dist_thresh:
17 | Threshold on the Euclidean distance between points.
18 |
19 | Returns
20 | -------
21 | torch.Tensor
22 | Support matrix. Size B x N x M.
23 |
24 | """
25 | # Squared l2 distance between points points of both point clouds
26 | distance_matrix = torch.sum(pcloud1 ** 2, -1, keepdim=True)
27 | distance_matrix = distance_matrix + torch.sum(pcloud2 ** 2, -1, keepdim=True).transpose(1, 2)
28 | distance_matrix = distance_matrix - 2 * torch.bmm(pcloud1, pcloud2.transpose(1, 2))
29 |
30 | # Force transport to be zero for points further than 10 m apart
31 | support_mat = (distance_matrix < dist_thresh ** 2).float()
32 |
33 | return support_mat
34 |
35 |
36 | def get_similarity_matrix(feature1, feature2):
37 | """
38 | Cosine similarity between point cloud features
39 |
40 | Parameters
41 | ----------
42 | feature1 : torch.Tensor
43 | Feature for points cloud 1. Used to computed transport cost. Size B x N x C.
44 | feature2 : torch.Tensor
45 | Feature for points cloud 2. Used to computed transport cost. Size B x M x C.
46 |
47 | Returns
48 | -------
49 | torch.Tensor
50 | Feature similarity matrix. Size B x N x M.
51 | """
52 | # Normalize features
53 | feature1_normalized = feature1 / (feature1.norm(dim=-1, p=2, keepdim=True) + 1e-8)
54 | feature2_normalized = feature2 / (feature2.norm(dim=-1, p=2, keepdim=True) + 1e-8)
55 |
56 | # Compute feature similarity
57 | sim_mat = torch.bmm(feature1_normalized, feature2_normalized.transpose(1, 2))
58 |
59 | return sim_mat
60 |
61 |
62 | def normalize_mat(mat, mat_normalization, dim=None):
63 | """
64 | The method to normalize the input matrix to be like a statistical matrix.
65 | """
66 | if dim is None:
67 | dim = 1 if len(mat.shape) == 3 else 0
68 |
69 | if mat_normalization == "none":
70 | return mat
71 | if mat_normalization == "softmax":
72 | return F.softmax(mat, dim=dim)
73 | raise NameError
74 |
75 |
76 | def get_s_t_topk(mat, k, s_only=False, nn_idx=None):
77 | """
78 | Get nearest neighbors per point (similarity value and index) for source and target shapes
79 |
80 | Args:
81 | mat (BxNsxNb Tensor): Similarity matrix
82 | k: Number of neighbors per point
83 | s_only: Whether to get neighbors only for the source point cloud or also for the target point cloud.
84 | nn_idx: An optional pre-computed nearest neighbor indices.
85 | """
86 | if nn_idx is not None:
87 | assert s_only, "Pre-computed nearest neighbor indices is allowed for the source point cloud only."
88 | s_nn_idx = nn_idx
89 | s_nn_val = mat.gather(dim=2, index=nn_idx)
90 | t_nn_val = t_nn_idx = None
91 | else:
92 | s_nn_val, s_nn_idx = mat.topk(k=min(k, mat.shape[2]), dim=2)
93 |
94 | if not s_only:
95 | t_nn_val, t_nn_idx = mat.topk(k=k, dim=1)
96 |
97 | t_nn_val = t_nn_val.transpose(2, 1)
98 | t_nn_idx = t_nn_idx.transpose(2, 1)
99 | else:
100 | t_nn_val = None
101 | t_nn_idx = None
102 |
103 | return s_nn_val, s_nn_idx, t_nn_val, t_nn_idx
104 |
105 |
106 | def get_s_t_neighbors(k, mat, sim_normalization, s_only=False, ignore_first=False, nn_idx=None):
107 | s_nn_sim, s_nn_idx, t_nn_sim, t_nn_idx = get_s_t_topk(mat, k, s_only=s_only, nn_idx=nn_idx)
108 | if ignore_first:
109 | s_nn_sim, s_nn_idx = s_nn_sim[:, :, 1:], s_nn_idx[:, :, 1:]
110 |
111 | s_nn_weight = normalize_mat(s_nn_sim, sim_normalization, dim=2)
112 |
113 | if not s_only:
114 | if ignore_first:
115 | t_nn_sim, t_nn_idx = t_nn_sim[:, :, 1:], t_nn_idx[:, :, 1:]
116 |
117 | t_nn_weight = normalize_mat(t_nn_sim, sim_normalization, dim=2)
118 | else:
119 | t_nn_weight = None
120 |
121 | return s_nn_weight, s_nn_sim, s_nn_idx, t_nn_weight, t_nn_sim, t_nn_idx
122 |
123 |
124 | def knn(x, k):
125 | inner = -2 * torch.matmul(x.transpose(2, 1), x)
126 | xx = torch.sum(x ** 2, dim=1, keepdim=True)
127 | pairwise_distance = -xx - inner - xx.transpose(2, 1)
128 |
129 | idx = pairwise_distance.topk(k=k, dim=-1)[1] # (batch_size, num_points, k)
130 |
131 | return idx
132 |
133 |
134 | def get_graph_feature(x, k, idx=None, only_intrinsic='neighs', permute_feature=True):
135 | batch_size = x.size(0)
136 | num_points = x.size(2)
137 | x = x.view(batch_size, -1, num_points)
138 | if idx is None:
139 | idx = knn(x, k=k) # (batch_size, num_points, k)
140 | else:
141 | if len(idx.shape) == 2:
142 | idx = idx.unsqueeze(0).repeat(batch_size, 1, 1)
143 | idx = idx[:, :, :k]
144 | k = min(k, idx.shape[-1])
145 |
146 | num_idx = idx.shape[1]
147 |
148 | idx_base = torch.arange(0, batch_size, device=idx.device).view(-1, 1, 1) * num_points
149 |
150 | idx = idx + idx_base
151 |
152 | idx = idx.contiguous()
153 | idx = idx.view(-1)
154 |
155 | _, num_dims, _ = x.size()
156 |
157 | 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)
158 | feature = x.view(batch_size * num_points, -1)[idx, :]
159 | feature = feature.view(batch_size, num_idx, k, num_dims)
160 | x = x.view(batch_size, num_points, 1, num_dims).repeat(1, 1, k, 1)
161 |
162 | if only_intrinsic == 'true':
163 | feature = feature - x
164 | elif only_intrinsic == 'neighs':
165 | feature = feature
166 | elif only_intrinsic == 'concat':
167 | feature = torch.cat((feature, x), dim=3)
168 | else:
169 | feature = torch.cat((feature - x, x), dim=3)
170 |
171 | if permute_feature:
172 | feature = feature.permute(0, 3, 1, 2).contiguous()
173 |
174 | return feature
175 |
176 |
177 | def reconstruct(pos, nn_idx, nn_weight, k):
178 | nn_pos = get_graph_feature(pos.transpose(1, 2), k=k, idx=nn_idx, only_intrinsic='neighs', permute_feature=False)
179 | nn_weighted = nn_pos * nn_weight.unsqueeze(dim=3)
180 | recon = torch.sum(nn_weighted, dim=2)
181 |
182 | return recon
183 |
--------------------------------------------------------------------------------
/tools/seed.py:
--------------------------------------------------------------------------------
1 | """Helper functions to help with reproducibility of models. """
2 |
3 | import logging
4 | import os
5 | import random
6 | from typing import Optional
7 |
8 | import numpy as np
9 | import torch
10 |
11 | log = logging.getLogger(__name__)
12 |
13 |
14 | def seed_everything(seed: Optional[int] = None) -> int:
15 | """
16 | Function that sets seed for pseudo-random number generators in:
17 | pytorch, numpy, python.random
18 | In addition, sets the env variable `PL_GLOBAL_SEED` which will be passed to
19 | spawned subprocesses (e.g. ddp_spawn backend).
20 |
21 | Args:
22 | seed: the integer value seed for global random state in Lightning.
23 | If `None`, will read seed from `PL_GLOBAL_SEED` env variable
24 | or select it randomly.
25 | """
26 | max_seed_value = np.iinfo(np.uint32).max
27 | min_seed_value = np.iinfo(np.uint32).min
28 |
29 | try:
30 | if seed is None:
31 | seed = os.environ.get("PL_GLOBAL_SEED")
32 | seed = int(seed)
33 | except (TypeError, ValueError):
34 | seed = _select_seed_randomly(min_seed_value, max_seed_value)
35 | print(f"No correct seed found, seed set to {seed}")
36 |
37 | if not (min_seed_value <= seed <= max_seed_value):
38 | print(f"{seed} is not in bounds, numpy accepts from {min_seed_value} to {max_seed_value}")
39 | seed = _select_seed_randomly(min_seed_value, max_seed_value)
40 |
41 | log.info(f"Global seed set to {seed}")
42 | os.environ["PL_GLOBAL_SEED"] = str(seed)
43 | random.seed(seed)
44 | np.random.seed(seed)
45 | torch.manual_seed(seed)
46 | torch.cuda.manual_seed_all(seed)
47 | return seed
48 |
49 |
50 | def _select_seed_randomly(min_seed_value: int = 0, max_seed_value: int = 255) -> int:
51 | return random.randint(min_seed_value, max_seed_value)
52 |
--------------------------------------------------------------------------------
/tools/utils.py:
--------------------------------------------------------------------------------
1 | """ Helper utility functions. """
2 | import os
3 | import numpy as np
4 |
5 |
6 | def create_dir(directory):
7 | if not os.path.exists(directory):
8 | os.mkdir(directory)
9 | return directory
10 |
11 |
12 | def log_string(log_file, log_str):
13 | log_file.write(log_str + '\n')
14 | log_file.flush()
15 | print(log_str)
16 |
17 |
18 | def iterate_in_chunks(l, n):
19 | """ Yield successive 'n'-sized chunks from iterable 'l'.
20 | Note: last chunk will be smaller than l if n doesn't divide l perfectly.
21 | """
22 | for i in range(0, len(l), n):
23 | yield l[i: i + n]
24 |
25 |
26 | def load_data(load_path):
27 | pc_res = np.load(load_path)
28 |
29 | pc1, pc2, gt_flow, est_flow = pc_res['pc1'], pc_res['pc2'], pc_res['gt_flow_for_pc1'], pc_res['est_flow_for_pc1']
30 | try:
31 | gt_mask = pc_res['gt_mask_for_pc1']
32 | gt_mask = gt_mask.reshape(len(pc1))
33 | except KeyError:
34 | gt_mask = np.ones(len(pc1)) == 1
35 |
36 | try:
37 | corr_conf = pc_res['corr_conf_for_pc1']
38 | except KeyError:
39 | corr_conf = np.ones(len(pc1), dtype=np.float32)
40 |
41 | pc1_warped_gt = pc1 + gt_flow
42 | pc1_warped_est = pc1 + est_flow
43 |
44 | return pc1, pc2, gt_mask, gt_flow, est_flow, corr_conf, pc1_warped_gt, pc1_warped_est
45 |
46 |
--------------------------------------------------------------------------------
/tools/vis_utils.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | from mayavi import mlab
4 | from PIL import Image
5 |
6 |
7 | def get_view_params(mlab_fig):
8 | azimuth, elevation, distance, focalpoint = mlab_fig.view()
9 | roll = mlab_fig.roll()
10 |
11 | view_dict = {"azimuth": azimuth, "elevation": elevation, "distance": distance, "focalpoint": focalpoint, "roll": roll}
12 | return view_dict
13 |
14 |
15 | def plot_pc_list(pc_list, color_list, view_dict=None, get_view=False, fig=None, size=(1600, 800)):
16 | if fig is None:
17 | fig = mlab.figure(figure=None, bgcolor=(1, 1, 1), fgcolor=None, engine=None, size=size)
18 |
19 | for pc, color in zip(pc_list, color_list):
20 | if pc is not None:
21 | mlab.points3d(pc[:, 0], pc[:, 1], pc[:, 2], scale_factor=0.05, color=color, figure=fig)
22 |
23 | if view_dict is not None:
24 | mlab.view(**view_dict)
25 |
26 | if get_view:
27 | view_dict = get_view_params(mlab)
28 | print(view_dict)
29 |
30 | return fig
31 |
32 |
33 | def plot_pcs(pc1, pc2, mask=None, flow=None, color_dict=None, view_dict=None, get_view=False, fig=None, size=(1600, 800)):
34 | if color_dict is None:
35 | color_dict = {"pc1_c": (1, 0, 0), "pc2_c": (0, 1, 0), "flow_c": (0, 0, 1)}
36 |
37 | if fig is None:
38 | fig = mlab.figure(figure=None, bgcolor=(1, 1, 1), fgcolor=None, engine=None, size=size)
39 |
40 | num_points = len(pc1)
41 | if len(color_dict["pc1_c"]) == num_points: # color per-point:
42 | p3d = mlab.points3d(pc1[:, 0], pc1[:, 1], pc1[:, 2], scale_factor=0.05, figure=fig)
43 | p3d.glyph.scale_mode = 'scale_by_vector'
44 | p3d.mlab_source.dataset.point_data.scalars = color_dict["pc1_c"]
45 | else:
46 | mlab.points3d(pc1[:, 0], pc1[:, 1], pc1[:, 2], scale_factor=0.05, color=color_dict["pc1_c"], figure=fig)
47 |
48 | if flow is not None:
49 | if mask is not None:
50 | pc1_plot = pc1[mask]
51 | flow_plot = flow[mask]
52 | else:
53 | pc1_plot = pc1
54 | flow_plot = flow
55 | mlab.quiver3d(pc1_plot[:, 0], pc1_plot[:, 1], pc1_plot[:, 2], flow_plot[:, 0], flow_plot[:, 1], flow_plot[:, 2], scale_factor=1, color=color_dict["flow_c"], line_width=2.0, figure=fig)
56 | if pc2 is not None:
57 | mlab.points3d(pc2[:, 0], pc2[:, 1], pc2[:, 2], scale_factor=0.05, color=color_dict["pc2_c"], figure=fig)
58 |
59 | if view_dict is not None:
60 | mlab.view(**view_dict)
61 |
62 | if get_view:
63 | view_dict = get_view_params(mlab)
64 | print(view_dict)
65 |
66 | return fig
67 |
68 |
69 | def save_pc_plot(save_path, save=True, show=False):
70 | try:
71 | screenshot = mlab.screenshot()
72 | except:
73 | screenshot = mlab.screenshot()
74 | img = Image.fromarray(screenshot)
75 |
76 | if save:
77 | img.save(save_path)
78 |
79 | if show:
80 | mlab.show()
81 | else:
82 | mlab.close()
83 |
--------------------------------------------------------------------------------