Official Implementation of Neural Splines

Overview

Neural Splines: Fitting 3D Surfaces with Inifinitely-Wide Neural Networks

Neural Splines Teaser This repository contains the official implementation of the CVPR 2021 (Oral) paper Neural Splines: Fitting 3D Surfaces with Infinitely-Wide Neural Networks.

Setup Instructions

System Requirements

Neural Splines uses FALKON, a state-of-the-art kernel ridge regression solver to fit surfaces on one or more GPUs. We thus require at least one GPU to run Neural Splines. We additionally require a working version of the CUDA compiler nvcc. We recommend running this code on a machine with a lot of memory if you want to reconstruct large point clouds since Neural Splines stores an MxM preconditioner matrix in CPU memory (where M is the number of Nystrom samples).

Installing Dependencies

Neural splines has several dependencies which must be installed before it can be used. Some of these dependencies must be built and take time to install. There are three ways to install dependencies:

Installing Dependencies with conda

Simply run

conda env create -f environment.yml

and then go grab a coffee . When you get back, you will have a conda environment called neural-splines with the right dependencies installed.

Installing Dependencies with pip

We include several requirement-*.txt files in the requirements directory depending on your version of cuda. Choose the right file for your installation then run

pip install -r requirements/requirements-cuda<VERSION>.txt

and then go grab a coffee .

Installing Dependencies Manually (Not Recommended)

You will need to install the following dependencies manually to use this repository:

You will also need to build the following dependencies from source. The easiest way to do this is with pip (see commands below), but you can also clone the linked repositories and run setup.py install:

  • point-cloud-utils: pip install git+https://github.com/fwilliams/point-cloud-utils.git@neural-splines
  • FALKON: pip install git+https://github.com/fwilliams/falkon.git@kml
  • KeOpspip install git+https://github.com/fwilliams/keops.git@falkon

Testing Your Installation

⚠️ WARNING ⚠️ Due to a bug in KeOps, the first time you use any code in this repository will throw a ModuleNotFoundError. All subsequent invocations of Neural Splines should work.

  1. Download and unzip the example point clouds here
  2. Unzip the file, in the directory of this repository, which should produce a directory named demo_data
  3. Run python fit.py demo_data/bunny.ply 10_000 128 On the first run this will fail (see above, just rerun it). On the second run it will compile some kernels and then produce a file called recon.ply which should be a reconstructed Stanford Bunny. The image below shows the input points and reconstruction for the bunny,
  4. Run python fit-grid.py demo_data/living_room_33_500_per_m2.ply 10_000 512 8 which will produce another recon.ply mesh, this time of a full room as shown below.

A reconstructed Stanford Bunny A reconstruced living room

Using Neural Splines from the Command Line

There are two scripts in this repository to fit surfaces from the command line:

  • fit.py fits an input point cloud using a single Neural Spline. This method is good for smaller inputs without too much geometric complexity.
  • fit_grid.py fits an input point cloud in chunks using a different Neural Spline per chunk. This method is better for very large scenes with a lot of geometric complexity.

Reconstructing a point cloud with fit.py

fit.py fits an input point cloud using a single Neural Spline. This approach works best for relatively small inputs which don't have too much geometric complexity. fit.py takes least the following arguments

fit.py <INPUT_POINT_CLOUD> <NUM_NYSTROM_SAMPLES> <GRID_SIZE>

where

  • <INPUT_POINT_CLOUD> is a path to a PLY file containing 3D points and corresponding normals
  • <EPS> is a spacing parameter used for finite difference approximation of the gradient term in the kernel. To capture all surface details this should be less than half the smallest distance between two points. Generally setting this to values smalelr than 0.5/grid_size is reasonable for this parameter
  • <NUM_NYSTROM_SAMPLES> is the number of points to use as basis centers. A larger number of Nystrom samples will yield a more accurate reconstruction but increase runtime and CPU memory usage. Generally good values for this are between 10*sqrt(N) and 100*sqrt(N) where N is the number of input points.
  • <GRID_SIZE> is the number of voxel cells along the longest axis of the bounding box on which the reconstructed function gets sampled. For example if <grid_size> is 128 and the bounding box of the input pointcloud has dimensions [1, 0.5, 0.5], then we will sample the function on a 128x64x64 voxel grid before extracting a mesh.

Reconstructing very large point clouds with fit-grid.py

fit-grid.py fits an input point cloud in chunks using a different Neural Spline per chunk. This approach works well when the input point cloud is large or has a lot of geometric complexity. fit-grid.py takes the following required arguments

fit-grid.py <INPUT_POINT_CLOUD> <NUM_NYSTROM_SAMPLES> <GRID_SIZE> <CELLS_PER_AXIS>

where

  • <INPUT_POINT_CLOUD> is a path to a PLY file containing 3D points and corresponding normals
  • <NUM_NYSTROM_SAMPLES> is the number of points to use as basis centers within each chunk. A larger number of Nystrom samples will yield a more accurate reconstruction but increase runtime and CPU memory usage.
  • <GRID_SIZE> is the number of voxel cells along the longest axis of the bounding box on which the reconstructed function gets sampled. For example if <GRID_SIZE> is 128 and the bounding box of the input pointcloud has dimensions [1, 0.5, 0.5], then we will sample the function on a 128x64x64 voxel grid before extracting a mesh.
  • <CELLS_PER_AXIS> is an integer specifying the number of chunks to use along each axis. E.g. if <cells-per-axis> is 8, we will reconstruct the surface using 8x8x8 chunks.

Furthermore, fit-grid.py accepts the following optional arguments:

  • --overlap <OVERLAP> optionally specify the fraction by which cells overlap. The default value is 0.25. If this value is too small, there may be artifacts in the output at the boundary of cells.
  • --weight-type <WEIGHT_TYPE> How to interpolate predictions in overlapping cells. Must be one of 'trilinear' or 'none', where 'trilinear' interpolates using a partition of unity defined using a bicubic spline and 'none' does not interpolate overlapping cells. Default is 'trilinear'.
  • --min-pts-per-cell <MIN_PTS_PER_CELL> Ignore cells with fewer points than this value. Default is 0.

Additional arguments to fit.py and fit-grid.py

Additionally, both fit.py and fit-grid.py accept the following optional arguments which can alter the behavior and performance of the fitting process:

  • --scale <SCALE>: Reconstruct the surface in a bounding box whose diameter is --scale times bigger than the diameter of the bounding box of the input points. Defaults is 1.1.
  • --regularization <REGULARIZATION>: Regularization penalty for kernel ridge regression. Default is 1e-10.
  • --nystrom-mode <NYSTROM_MODE>: How to generate nystrom samples. Default is 'blue-noise'. Must be one of
    • 'random': choose Nyström samples at random from the input
    • 'blue-noise': downsample the input with blue noise to get Nyström samples
    • 'k-means': use k-means clustering to generate Nyström samples
  • --trim <TRIM>: If set to a positive value, trim vertices of the reconstructed mesh whose nearest point in the input is greater than this value. The units of this argument are voxels (where the grid_size determines the size of a voxel) Default is -1.0.
  • --eps <EPS>: Perturbation amount for finite differencing in voxel units. i.e. we perturb points by eps times the diagonal length of a voxel (where the grid_size determines the size of a voxel). To approximate the gradient of the function, we sample points +/- eps along the normal direction.
  • --voxel-downsample-threshold <VOXEL_DOWNSAMPLE_THRESHOLD>: If the number of input points is greater than this value, downsample it by averaging points and normals within voxels on a grid. The size of the voxel grid is determined via the --grid-size argument. Default is 150_000.NOTE: This can massively speed up reconstruction for very large point clouds and generally won't throw away any details.
  • --kernel <KERNEL>: Which kernel to use. Must be one of 'neural-spline', 'spherical-laplace', or 'linear-angle'. Default is 'neural-spline'.NOTE: The spherical laplace is a good approximation to the neural tangent kernel (see this paper for details)
  • --seed <SEED>: Random number generator seed to use.
  • --out <OUT>: Path to file to save reconstructed mesh in.
  • --save-grid: If set, save the function evaluated on a voxel grid to {out}.grid.npy where out is the value of the --out argument.
  • --save-points: If set, save the tripled input points, their occupancies, and the Nyström samples to an npz file named {out}.pts.npz where out is the value of the --out argument.
  • --cg-max-iters <CG_MAX_ITERS>: Maximum number of conjugate gradient iterations. Default is 20.
  • --cg-stop-thresh <CG_STOP_THRESH>: Stop threshold for the conjugate gradient algorithm. Default is 1e-5.
  • --dtype DTYPE: Scalar type of the data. Must be one of 'float32' or 'float64'. Warning: float32 only works for very simple inputs.
  • --outer-layer-variance <OUTER_LAYER_VARIANCE>: Variance of the outer layer of the neural network from which the neural spline kernel arises from. Default is 0.001.
  • --verbose: If set, spam your terminal with debug information

Trimming Reconstructed Meshes

Neural Splines can sometimes add surface sheets far away from input points, to remove these, we include a surface trimming script (similar to Poisson Surface Reconstruction), which trims mesh faces away from the input points. To trim a surface, simply run:

python trim-surface.py <INPUT_POINT_CLOUD> <RECONSTRUCTED_MESH> <GRID_SIZE> <DISTANCE_THRESHOLD> --out <OUT_FILE>

where:

  • <INPUT_POINT_CLOUD> is a path to the input point cloud to the reconstruction algorithm
  • <RECONSTRUCTED_MESH> is a path to the mesh reconstructed by neural splines
  • <GRID_SIZE> is the size of the voxel grid used to reconstruct the mesh (the same value as the <GRID_SIZE> argument to fit.py or fit-grid.py)
  • <DISTANCE_THRESHOLD> is the distance (in voxels) above which faces should be discarded (e.g. passing 2.5 will discard any surface which is greater than 3 voxels away from an input point.
  • --out <OUT_FILE> is an optional path to save the trimmed mesh to. By default it is trimmed.ply.

Using Neural Splines in Python

Neural Splines can be used directly from within python by importing the neural_splines module in this repository.

To reconstruct a surface using Neural Splines, use the function neural_splines.fit_model_to_pointcloud. It returns a model object with the same API as Skikit-Learn. NOTE: neural_splines.fit_model_to_pointcloud can additionally accept other optional arguments. Run help(neural_splines.fit_model_to_pointcloud) for details.

from neural_splines import fit_model_to_point_cloud

# x is a point cloud stored in a torch tensor of shape [N, 3]
# n is a tensor of unit normals (one per point) of shape [N, 3]
# num_ny is the number of Nystrom samples to use 
# eps is the finite differencing coefficient (see documentation above)
model = fit_model_to_pointcloud(x, n, num_ny, eps)

# Evaluate the neural spline at a point p
p = torch.tensor([[0.5, 0.5, 0.5]]).to(x)
f_p = model.predict(p)

To evaluate a fitted Neural Spline on a grid of points, you can use the function neural_splines.eval_model_on_grid. NOTE: neural_splines.eval_model_on_grid can also accept other optional arguments, run help(neural_splines.eval_model_on_grid) for details.

from neural_splines import eval_model_on_grid

# Assume model is a Neural Spline fitted with fit_model_to_point_cloud

# Bounding box of the point cloud x represented as a tuple (origin, size)
bbox = x.min(0)[0], x.max(0)[0] - x.min(0)[0]
grid_res = torch.tensor([128, 128, 128]).to(torch.int32)

recon = eval_model_on_grid(model, bbox, voxel_grid_size)  # a [128, 128, 128] shaped tensor representing the neural spline evaluated on a grid.
You might also like...
Official PyTorch implementation of
Official PyTorch implementation of "Rapid Neural Architecture Search by Learning to Generate Graphs from Datasets" (ICLR 2021)

Rapid Neural Architecture Search by Learning to Generate Graphs from Datasets This is the official PyTorch implementation for the paper Rapid Neural A

Official implementation of the ICML2021 paper
Official implementation of the ICML2021 paper "Elastic Graph Neural Networks"

ElasticGNN This repository includes the official implementation of ElasticGNN in the paper "Elastic Graph Neural Networks" [ICML 2021]. Xiaorui Liu, W

Official implementation of NPMs: Neural Parametric Models for 3D Deformable Shapes - ICCV 2021
Official implementation of NPMs: Neural Parametric Models for 3D Deformable Shapes - ICCV 2021

NPMs: Neural Parametric Models Project Page | Paper | ArXiv | Video NPMs: Neural Parametric Models for 3D Deformable Shapes Pablo Palafox, Aljaz Bozic

Official implementation of the paper ``Unifying Nonlocal Blocks for Neural Networks'' (ICCV'21)
Official implementation of the paper ``Unifying Nonlocal Blocks for Neural Networks'' (ICCV'21)

Spectral Nonlocal Block Overview Official implementation of the paper: Unifying Nonlocal Blocks for Neural Networks (ICCV'21) Spectral View of Nonloca

Official PyTorch implementation of the paper: Improving Graph Neural Network Expressivity via Subgraph Isomorphism Counting.
Official PyTorch implementation of the paper: Improving Graph Neural Network Expressivity via Subgraph Isomorphism Counting.

Improving Graph Neural Network Expressivity via Subgraph Isomorphism Counting Official PyTorch implementation of the paper: Improving Graph Neural Net

The official implementation of the IEEE S&P`22 paper "SoK: How Robust is Deep Neural Network Image Classification Watermarking".

Watermark-Robustness-Toolbox - Official PyTorch Implementation This repository contains the official PyTorch implementation of the following paper to

This is an official PyTorch implementation of Task-Adaptive Neural Network Search with Meta-Contrastive Learning (NeurIPS 2021, Spotlight).
This is an official PyTorch implementation of Task-Adaptive Neural Network Search with Meta-Contrastive Learning (NeurIPS 2021, Spotlight).

NeurIPS 2021 (Spotlight): Task-Adaptive Neural Network Search with Meta-Contrastive Learning This is an official PyTorch implementation of Task-Adapti

Official implementation of Neural Bellman-Ford Networks (NeurIPS 2021)

NBFNet: Neural Bellman-Ford Networks This is the official codebase of the paper Neural Bellman-Ford Networks: A General Graph Neural Network Framework

Official Implementation of "LUNAR: Unifying Local Outlier Detection Methods via Graph Neural Networks"

LUNAR Official Implementation of "LUNAR: Unifying Local Outlier Detection Methods via Graph Neural Networks" Adam Goodge, Bryan Hooi, Ng See Kiong and

Comments
  • fit-grid.py requires tqdm module

    fit-grid.py requires tqdm module

    The tqdm module is not installed by the default procedure using conda or pip. Easy to fix manually, but would be better to have it in environment.yml or requirements, respectively.

    opened by nvibd 1
  • Main README.md references

    Main README.md references "eps" parameter

    The main read me uses the "eps" parameter in the tutorial example, as well as explaining what the parameter does further on. However, it seems like this parameter has been dropped from the code already, and pasting the tutorial example gives:

    $ python fit.py demo_data/bunny.ply 0.005 10_000 128
    usage: fit.py [-h] [--trim TRIM] [--eps EPS] [--scale SCALE]
                  [--regularization REGULARIZATION] [--nystrom-mode NYSTROM_MODE]
                  [--voxel-downsample-threshold VOXEL_DOWNSAMPLE_THRESHOLD]
                  [--kernel KERNEL] [--seed SEED] [--out OUT] [--save-grid]
                  [--save-points] [--cg-max-iters CG_MAX_ITERS]
                  [--cg-stop-thresh CG_STOP_THRESH] [--dtype DTYPE]
                  [--outer-layer-variance OUTER_LAYER_VARIANCE] [--use-abs-units]
                  [--verbose]
                  input_point_cloud num_nystrom_samples grid_size
    fit.py: error: argument num_nystrom_samples: invalid int value: '0.005'
    
    opened by nvibd 1
  • (I'm sure there is enough memory)falkon.cuda.cudart_gpu.CudaError: cudaErrorMemoryAllocation

    (I'm sure there is enough memory)falkon.cuda.cudart_gpu.CudaError: cudaErrorMemoryAllocation

    Using random seed 2787157593 Generating 10000 blue noise Nyström samples for 30000 points.


    FalkonOptions(keops_acc_dtype='auto', keops_sum_scheme='auto', keops_active='auto', chol_force_in_core=False, chol_force_ooc=False, chol_par_blk_multiplier=2, lauum_par_blk_multiplier=8, pc_epsilon_32=1e-05, pc_epsilon_64=1e-13, cpu_preconditioner=False, cg_epsilon_32=1e-07, cg_epsilon_64=1e-15, cg_tolerance=1e-07, cg_full_gradient_every=10, cg_print_when_done=True, debug=False, use_cpu=False, max_gpu_mem=inf, max_cpu_mem=inf, compute_arch_speed=False, no_single_kernel=True, min_cuda_pc_size_32=10000, min_cuda_pc_size_64=30000, min_cuda_iter_size_32=300000000, min_cuda_iter_size_64=900000000, never_store_kernel=False) Using Neural Spline Kernel cudaErrorMemoryAllocation Traceback (most recent call last): File "/data3/xusheng/neural-splines/fit.py", line 153, in main() File "/data3/xusheng/neural-splines/fit.py", line 118, in main model, tx = fit_model_to_pointcloud(x, n, num_ny=args.num_nystrom_samples, eps=eps_world_coords, File "/data3/xusheng/neural-splines/neural_splines/init.py", line 212, in fit_model_to_pointcloud model = _run_falkon_fit(x, y, reg, ny_count, center_selector, File "/data3/xusheng/neural-splines/neural_splines/init.py", line 118, in _run_falkon_fit model = falkon.Falkon(kernel=kernel, penalty=penalty, M=num_ny, options=falkon_opts, maxiter=maxiters, File "/data/xusheng/lib/python3.9/site-packages/falkon-0.6.1-py3.9-linux-x86_64.egg/falkon/models/falkon.py", line 113, in init self._init_cuda() File "/data/xusheng/lib/python3.9/site-packages/falkon-0.6.1-py3.9-linux-x86_64.egg/falkon/models/model_utils.py", line 69, in _init_cuda initialization.init(self._base_opt) File "/data/xusheng/lib/python3.9/site-packages/falkon-0.6.1-py3.9-linux-x86_64.egg/falkon/cuda/initialization.py", line 59, in init device_ids = [k for k in get_device_info(opt).keys() if k >= 0] File "/data/xusheng/lib/python3.9/site-packages/falkon-0.6.1-py3.9-linux-x86_64.egg/falkon/utils/devices.py", line 194, in get_device_info __COMP_DATA = _get_gpu_device_info(opt, g, __COMP_DATA) File "/data/xusheng/lib/python3.9/site-packages/falkon-0.6.1-py3.9-linux-x86_64.egg/falkon/utils/devices.py", line 82, in _get_gpu_device_info mem_free, mem_total = cuda_meminfo(g) File "/data/xusheng/lib/python3.9/site-packages/falkon-0.6.1-py3.9-linux-x86_64.egg/falkon/cuda/cudart_gpu.py", line 233, in cuda_meminfo free, total = cudaMemGetInfo() File "/data/xusheng/lib/python3.9/site-packages/falkon-0.6.1-py3.9-linux-x86_64.egg/falkon/cuda/cudart_gpu.py", line 226, in cudaMemGetInfo cuda_check_status(status) File "/data/xusheng/lib/python3.9/site-packages/falkon-0.6.1-py3.9-linux-x86_64.egg/falkon/cuda/cudart_gpu.py", line 174, in cuda_check_status raise CudaError(status) falkon.cuda.cudart_gpu.CudaError: cudaErrorMemoryAllocation

    opened by sxs505 0
  • Using too high min-pts-per-cell leads to ValueError

    Using too high min-pts-per-cell leads to ValueError

    Here's the full command line and resulting error. I have attached the input .PLY file.

    $ python fit-grid.py '/home/user/Documents/meshing_comp/input_files/einstein.ply' 10_000 128 8 --min-pts-per-cell 1000
    Using random seed 2682126288
    Downsampling input point cloud to voxel resolution.
    Fitting 7549 points using 512 cells
    100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████▊| 511/512 [00:00<00:00, 2456.00it/s, Cell=(7, 7, 7), Num Points=0]fit-grid.py:202: DeprecationWarning: `np.bool` is a deprecated alias for the builtin `bool`. To silence this warning, use `bool` by itself. Doing this will not modify any behavior and is safe. If you specifically wanted the numpy scalar type, use `np.bool_` here.
    Deprecated in NumPy 1.20; for more details and guidance: https://numpy.org/devdocs/release/1.20.0-notes.html#deprecations
      eroded_mask = binary_erosion(out_mask.numpy().astype(np.bool), np.ones([3, 3, 3]).astype(np.bool))
    Traceback (most recent call last):
      File "fit-grid.py", line 224, in <module>
        main()
      File "fit-grid.py", line 204, in main
        gradient_direction='ascent')
      File "/home/user/miniconda3/envs/neural-splines/lib/python3.7/site-packages/skimage/measure/_marching_cubes_lewiner.py", line 137, in marching_cubes
        mask=mask)
      File "/home/user/miniconda3/envs/neural-splines/lib/python3.7/site-packages/skimage/measure/_marching_cubes_lewiner.py", line 302, in _marching_cubes_lewiner
        raise ValueError("Surface level must be within volume data range.")
    ValueError: Surface level must be within volume data range.
    100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 512/512 [00:00<00:00, 1791.14it/s, Cell=(7, 7, 7), Num Points=0]
    

    einstein.zip

    opened by nvibd 1
Owner
Francis Williams
I am a PhD student in the Math and Data Group and the Geometric Computing Lab at NYU advised by Joan Bruna and Denis Zorin.
Francis Williams
The official implementation of NeMo: Neural Mesh Models of Contrastive Features for Robust 3D Pose Estimation [ICLR-2021]. https://arxiv.org/pdf/2101.12378.pdf

NeMo: Neural Mesh Models of Contrastive Features for Robust 3D Pose Estimation [ICLR-2021] Release Notes The offical PyTorch implementation of NeMo, p

Angtian Wang 76 Nov 23, 2022
Official PyTorch implementation of Joint Object Detection and Multi-Object Tracking with Graph Neural Networks

This is the official PyTorch implementation of our paper: "Joint Object Detection and Multi-Object Tracking with Graph Neural Networks". Our project website and video demos are here.

Richard Wang 443 Dec 6, 2022
This project is the official implementation of our accepted ICLR 2021 paper BiPointNet: Binary Neural Network for Point Clouds.

BiPointNet: Binary Neural Network for Point Clouds Created by Haotong Qin, Zhongang Cai, Mingyuan Zhang, Yifu Ding, Haiyu Zhao, Shuai Yi, Xianglong Li

Haotong Qin 59 Dec 17, 2022
Official PyTorch implementation of "ArtFlow: Unbiased Image Style Transfer via Reversible Neural Flows"

ArtFlow Official PyTorch implementation of the paper: ArtFlow: Unbiased Image Style Transfer via Reversible Neural Flows Jie An*, Siyu Huang*, Yibing

null 123 Dec 27, 2022
Official implementation of Rethinking Graph Neural Architecture Search from Message-passing (CVPR2021)

Rethinking Graph Neural Architecture Search from Message-passing Intro The GNAS can automatically learn better architecture with the optimal depth of

Shaofei Cai 48 Sep 30, 2022
Official implementation for NIPS'17 paper: PredRNN: Recurrent Neural Networks for Predictive Learning Using Spatiotemporal LSTMs.

PredRNN: A Recurrent Neural Network for Spatiotemporal Predictive Learning The predictive learning of spatiotemporal sequences aims to generate future

THUML: Machine Learning Group @ THSS 243 Dec 26, 2022
Official Pytorch implementation of 'GOCor: Bringing Globally Optimized Correspondence Volumes into Your Neural Network' (NeurIPS 2020)

Official implementation of GOCor This is the official implementation of our paper : GOCor: Bringing Globally Optimized Correspondence Volumes into You

Prune Truong 71 Nov 18, 2022
Official implementation of GraphMask as presented in our paper Interpreting Graph Neural Networks for NLP With Differentiable Edge Masking.

GraphMask This repository contains an implementation of GraphMask, the interpretability technique for graph neural networks presented in our ICLR 2021

Michael Schlichtkrull 29 Sep 2, 2022
The official PyTorch implementation of recent paper - SAINT: Improved Neural Networks for Tabular Data via Row Attention and Contrastive Pre-Training

This repository is the official PyTorch implementation of SAINT. Find the paper on arxiv SAINT: Improved Neural Networks for Tabular Data via Row Atte

Gowthami Somepalli 284 Dec 21, 2022
Official PyTorch implementation of the preprint paper "Stylized Neural Painting", accepted to CVPR 2021.

Official PyTorch implementation of the preprint paper "Stylized Neural Painting", accepted to CVPR 2021.

Zhengxia Zou 1.5k Dec 28, 2022