Library / SDK
naver/roma avatar
naver/roma

roma: a lightweight PyTorch library for 3D rotations

RoMa: A lightweight library to deal with 3D rotations in PyTorch.

646 stars28 forksPythonNOASSERTION

At a glance

What is it?
RoMa, short for Rotation Manipulation, gives PyTorch users differentiable conversions between rotation representations, a way to project an arbitrary matrix back onto the rotation manifold, and the utilities that go with them. It is small, it is differentiable throughout, and it asks you to cite a paper when you publish.
Who is it for?
RoMa fits a PyTorch project that has to learn or optimise over rotations and wants the conversions to be differentiable and batched, rather than treating a 3x3 matrix as an unconstrained parameter and hoping. It is the wrong choice if you only need to multiply two quaternions, and the wrong choice if a citation requirement in your publication policy is a problem, since the README asks you to cite the underlying paper.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Rotation Manipulation, aimed at gradient-based work

The README describes RoMa, which stands for Rotation Manipulation, as providing differentiable mappings between 3D rotation representations, mappings from Euclidean to rotation space, and various utilities related to rotations. It is implemented in PyTorch and aims to be an easy-to-use and reasonably efficient toolbox for machine learning and gradient-based optimisation.

That phrase, gradient-based optimisation, is the reason the library exists and the key to reading its API. A rotation is not a 3x3 matrix in the way a network weight is a 3x3 matrix. The set of valid rotations is a manifold, and a matrix that has drifted off it is no longer a rotation. If you parameterise a rotation with a matrix and train through it, nothing in the computation tells you the matrix stopped being orthonormal, and the gradient you receive points into a space that contains rotations which do not exist.

The mappings are differentiable, so a network can predict a rotation directly and be trained end to end without an unrolled projection step in the graph. The other half of the design is the reverse direction, from Euclidean space into rotation space, which is what a regression head needs.

The project comes from NAVER, and the package manifest names Romain Brégier as the author, who also wrote the paper the README asks you to cite.

Batch dimensions are a first-class argument

The example in the README is the best introduction to the API, and it opens with a comment about convenience:

python
import torch
import roma

# Arbitrary numbers of batch dimensions are supported, for convenience.
batch_shape = (2, 3)

# Conversion between rotation representations
rotvec = torch.randn(batch_shape + (3,))
q = roma.rotvec_to_unitquat(rotvec)
R = roma.unitquat_to_rotmat(q)
Rbis = roma.rotvec_to_rotmat(rotvec)
euler_angles = roma.unitquat_to_euler("xyz", q, degrees=True)

The first block is conversions. A rotation vector becomes a unit quaternion with `rotvec_to_unitquat`, a unit quaternion becomes a rotation matrix with `unitquat_to_rotmat`, and there is a direct rotation-vector to matrix call as well, which is the one you would use when you do not care about the intermediate representation. The Euler conversion takes the axis convention as its first argument and a `degrees=True` flag, so the angle convention is explicit rather than assumed.

The second thing to notice is the shape convention. `batch_shape` is a tuple of two dimensions and every tensor is built by appending the vector dimension to it, so a batch of 2 by 3 items of 3 numbers is a tensor of shape 2 by 3 by 3. The library then works along the last dimension throughout.

That is the design decision that makes the library usable in a real training loop. Nothing has to be reshaped by hand, and the same code handles a single rotation, a batch of rotations, and a batch of batches of rotations.

Projecting an arbitrary matrix back onto a rotation

The regression part of the example is the part worth studying, because it is the actual research contribution:

python
# Regression of a rotation from an arbitrary input:
# Special Procrustes orthonormalization of a 3x3 matrix
R1 = roma.special_procrustes(torch.randn(batch_shape + (3, 3)))
# Conversion from a 6D representation
R2 = roma.special_gramschmidt(torch.randn(batch_shape + (3, 2)))
# From the 10 coefficients of a 4x4 symmetric matrix
q = roma.symmatrixvec_to_unitquat(torch.randn(batch_shape + (10,)))

Three different input shapes, three different projections, one output that is guaranteed to be a rotation.

`special_procrustes` takes an arbitrary 3x3 matrix and orthonormalises it, which is the classic way to get the closest rotation to an unconstrained matrix. `special_gramschmidt` takes a 3x2 matrix, which is the popular continuous 6D rotation representation, and turns it into a rotation by Gram-Schmidt. `symmatrixvec_to_unitquat` takes the ten coefficients of the upper triangular part of a symmetric matrix and returns a unit quaternion, which is the route used when the rotation is a symmetric function of a larger SPD matrix.

The alternative to all of this is to predict a matrix and treat it as a rotation, which is simpler to write and wrong in a way that only shows up as training that does not converge or a model that produces matrices that are nearly but not quite rotations. RoMa's contribution is to make the projection a differentiable, batched, tested operation rather than a per-sample numpy call.

Geodesic distance and quaternion algebra

The metrics section of the example covers the questions you ask about two rotations:

python
# Metrics on the rotation space
R1, R2 = roma.random_rotmat(size=5), roma.random_rotmat(size=5)
theta = roma.utils.rotmat_geodesic_distance(R1, R2)
cos_theta = roma.utils.rotmat_cosine_angle(R1.transpose(-2, -1) @ R2)

# Operations on quaternions
q_identity = roma.quat_product(roma.quat_conjugation(q), q)

`rotmat_geodesic_distance` returns the angle of the shortest rotation taking one matrix to the other, which is the natural distance on the rotation manifold and the one you want for a loss function. `rotmat_cosine_angle` gives the cosine of that angle instead, computed from the product of one transpose with the other, which is a cheaper quantity to evaluate when that is all you need.

The quaternion operations are the standard algebra, and the second line is a test in one expression: conjugating a unit quaternion and multiplying it by the quaternion should give the identity, so `q_identity` is the identity quaternion up to floating point.

The `utils` submodule is where these live, which is a small but useful structural choice. Conversion functions sit at the top level of the package as the primary API, and the measurement and auxiliary helpers live under `roma.utils`, so the import list at the top of your file tells you whether you are using the core or the extras.

Slerp, rigid transforms and homogeneous matrices

The last part of the example is about motion rather than about representation:

python
# Spherical interpolation between rotation vectors (shortest path)
rotvec0, rotvec1 = torch.randn(batch_shape + (3,)), torch.randn(batch_shape + (3,))
rotvec_interpolated = roma.rotvec_slerp(rotvec0, rotvec1, steps)

# Rigid transformation T composed of a rotation part R and a translation part t
t = torch.randn(batch_shape + (3,))
T = roma.Rigid(R, t)
# Composing and inverting transformations
identity = T @ T.inverse()
# Casting the result to a batch of 4x4 homogeneous matrices
M = identity.to_homogeneous()

The interpolation call takes the number of steps explicitly, which means the interpolation is a batched operation that produces a fixed-size result rather than a generator, and it is done along the shortest path, as the comment says.

The `Rigid` object is the other piece worth noting. A rigid transformation is a rotation and a translation treated as one thing, and the API uses the operator that a reader would expect: composing two transformations is `@`, inverting is `.inverse()`, and casting to the 4x4 homogeneous form is `.to_homogeneous()`. The line `identity = T @ T.inverse()` is a self-check, since the product of a transform and its inverse is the identity.

For a library of this size, the ergonomics of these two lines matter more than they might appear. The alternative is assembling 4x4 matrices by hand, with the rotation in the top-left corner and the translation in the last column, and getting the convention wrong.

Install from pip or from the repository

Installation is one line either way. The easiest way is pip:

bash
pip install roma

and the alternative is to install the latest version directly from the source repository:

bash
pip install git+https://github.com/naver/roma

The packaging metadata tells you what you get. The project is named `roma`, the version is dynamic and derived by setuptools_scm from the repository state, and the build requires setuptools 64 or newer along with setuptools_scm and wheel. The runtime dependencies are `torch>=2.0` and `numpy`, and `requires-python` is `>=3.8`.

Torch is the dependency that matters, since the whole library is torch operations. The declared floor of 2.0 is what you should check against your environment, particularly if you are on an older CUDA-compatible release, and the fact that the version is dynamic means a pip install from the repository gives you the current commit rather than a tagged release.

The development group is small: ruff pinned at 0.16.2, sphinx and the sphinx-rtd-theme for the documentation. The ruff configuration sets a line length of 120 and pins the rule set explicitly to E4, E7, E9 and F, with E741 ignored because the library uses I and O as identifiers for the identity matrix and other math names, and per-file ignores for the star re-exports in the package init files.

BSD-3-Clause, SciPy code, and a citation you are asked for

The licence is the 3-Clause BSD License, with the README stating that RoMa, Copyright 2020 NAVER Corp., is licensed under it and pointing at the LICENSE file. The repository listing records the licence as one that GitHub cannot classify, so the LICENSE file in the repository is the text to read.

Two further notes come from the same section. Bits of code were adapted from SciPy, and the documentation is generated, distributed and displayed with the support of Sphinx and other materials, with a NOTICE file and a NOTICE_DOCUMENTATION directory at the root for those attributions. The repository has a build_doc.sh and a build_and_push_doc.sh script, so the published documentation at naver.github.io/roma is built from the repository rather than maintained by hand.

The References section is the part to note. For a more in-depth discussion of differentiable mappings on the rotation space it points at Deep Regression on Manifolds: a 3D Rotation Case Study, by Romain Brégier, in the 2021 International Conference on 3D Vision, and it asks you to cite that work in your publications, with the BibTeX entry given in the README.

The examples directory supports the same intent. Alongside example.py and a snippets directory there are geodesic_distance_comparison.py, linearity_deviations.py and mapping_benchmark.py, which are the three things you would want to check yourself: whether the distance function behaves as you expect against a reference, how far the mappings deviate from linearity, and how the different representations compare in cost.

The repository is not archived, the last push was on 2026-10-16, and there are no GitHub releases, so the PyPI version is the release marker and the paper is the reference.

Editorial conclusion

RoMa fits a PyTorch project that has to learn or optimise over rotations and wants the conversions to be differentiable and batched, rather than treating a 3x3 matrix as an unconstrained parameter and hoping. It is the wrong choice if you only need to multiply two quaternions, and the wrong choice if a citation requirement in your publication policy is a problem, since the README asks you to cite the underlying paper. Verify first that your torch version satisfies the declared floor of 2.0, that the representation you plan to regress from is one of the supported inputs, and that the generated documentation matches the version you pin.

Frequently asked questions

What is RoMa used for in PyTorch?

RoMa, short for Rotation Manipulation, provides differentiable mappings between 3D rotation representations, mappings from Euclidean to rotation space, and utilities related to rotations, and is implemented in PyTorch for machine learning and gradient-based optimisation.

How do I install roma?

The README says the easiest way is pip install roma, and the alternative is installing the latest version directly from the source repository with pip install git+https://github.com/naver/roma. The runtime dependencies are torch>=2.0 and numpy.

How do I get a valid rotation from an unconstrained matrix in roma?

Use roma.special_procrustes, which performs a Special Procrustes orthonormalization of a 3x3 matrix, or roma.special_gramschmidt for the 6D representation given as a 3x2 matrix. The README also shows roma.symmatrixvec_to_unitquat for the ten coefficients of a symmetric matrix.

How do I measure the distance between two rotations with roma?

The utils module provides rotmat_geodesic_distance for the angle of the shortest rotation between two matrices, and rotmat_cosine_angle for the cosine of that angle. The example computes the cosine from R1.transpose(-2, -1) @ R2.

What licence is roma released under?

The 3-Clause BSD License, with the README stating that RoMa, Copyright 2020 NAVER Corp., is licensed under it. Bits of code were adapted from SciPy, and the documentation is built with Sphinx with a NOTICE file for those attributions.

Official sources

  1. Issues
  2. naver/roma on GitHub
  3. Project website
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/naver-roma.svg)](https://hysenlabs.com/projects/naver-roma)

Was this useful?