-
Notifications
You must be signed in to change notification settings - Fork 1.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support splat export in original dataset coordinates #2951
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
seems good
I don't think the option would work actually for spherical harmonic parameters. The spherical harmonics are saved in the transformed frame and cannot be "re-orient" back to the original coordinates. (We can apply translation and scale, but not rotations, otherwise SH becomes inconsistent.) My recommendation is to save the transforms as meta data as part of exporter. |
Agree the current implementation is flawed, will revisit this after ECCV deadline! |
…dio-project/nerfstudio into brent/splat_export_world_frame
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jb-ye Many viewers ignore spherical harmonics, and in my experience doing a rigid transform on splats looks plenty fine. I think ideally Nerfstudio just does a scale and re-center internally as part of the model rather than changing any of the camera poses, initial points etc that are part of the dataset. It should be up to the model to normalize as needed, not up to the user.
) | ||
) | ||
|
||
CONSOLE.log("Caching / undistorting eval images") | ||
with ThreadPoolExecutor() as executor: | ||
with ThreadPoolExecutor(max_workers=2) as executor: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please make this configurable? or maybe this is just for debugging
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm actually not sure why this shows up in this diff, the change is from #2969. it speeds up undistortion a lot for big datasets I've been toying with!
it's hardcoded to 2 because we can really only expect benefits from one thread doing IO while the other thread is doing undistortion; the implementation is still weird given this (ideally we'd just have 1 worker doing IO while the main one is sequentially undistorting) but I'm a fan of not letting perfect be the enemy of... better 🤷
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW each worker might need cv2.setNumThreads(1)
or else it can choke the CPU. I seem to see way more than 200% util here hence why i brought it up so maybe it's just not tuned well for all users.
maybe in a future refactor this stuff will just get pushed to a torch dataloader... the pinned memory part breaks for me for large datasets anyways, literally I got a OOM and hard lock-up because too too too much much much pinned memory
nerfstudio/scripts/exporter.py
Outdated
|
||
output_scale = 1 / dataparser_scale | ||
output_transform = np.zeros((3, 4)) | ||
output_transform[:3, :3] = dataparser_transform[:3, :3].T |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pretty please don't do transform math w/out at least comments, this sort of code is 110% likely to put a future reader in transform hell
also pretty please use pipeline.datamanager.train_dataparser_outputs.transform_poses_to_original_space()
because
(1) that's what's used elsewhere in this file
(2) using that function ensures future refactors won't break things, and most past nerfstudio refactors have indeed broken lots of things
nerfstudio/scripts/exporter.py
Outdated
np.einsum("ij,bj->bi", output_transform[:3, :3], model.means.cpu().numpy() * output_scale) | ||
+ output_transform[None, :3, 3] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please don't do this, ESPECIALLY w/out comments. I have lost a lot of time reading nerfstudio code that's like this. instead consider:
positions = model.means.cpu().numpy()
poses = np.eye(4, dtype=np.float32)[None, ...].repeat(positions.shape[0], axis=0)[:, :3, :]
poses[:, :3, 3] = positions
poses = pipeline.datamanager.train_dataparser_outputs.transform_poses_to_original_space(
torch.from_numpy(poses)
)
nerfstudio/scripts/exporter.py
Outdated
for i in range(3): | ||
map_to_tensors[f"scale_{i}"] = scales[:, i, None] | ||
|
||
quats = model.quats.data.cpu().numpy() | ||
def quaternion_multiply(wxyz0: np.ndarray, wxyz1: np.ndarray) -> np.ndarray: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
??? First of all, scipy.spatial.transform
already has quaternion multiply... at least this code is clear about scalar-first versus scalar-last.
this could be made more concise, but consider instead:
from scipy.spatial.transform import Rotation as ScR
# ns gplat says quaternions are [w,x,y,z] scalar-first format
# scipy is [x, y, z, w] scalar-last format
raw_quats = model.quats.data.cpu().numpy().squeeze()
R_quats = ScR.from_quat(raw_quats[:, [1, 2, 3, 0]])
# apply the inverse dataparser transform to the splat rotations
poses = np.eye(4, dtype=np.float32)[None, ...].repeat(raw_quats.shape[0], axis=0)[:, :3, :]
poses[:, :3, :3] = R_quats.as_matrix()
poses = pipeline.datamanager.train_dataparser_outputs.transform_poses_to_original_space(
torch.from_numpy(poses)
)
rots_in_input = poses[:, :3, :3].numpy()
quat_in_input = ScR.from_matrix(rots_in_input)
quats = quat_in_input.as_quat()[:, [3, 0, 1, 2], None]
Again, this uses transform_poses_to_original_space()
, which might amalgamate several different transforms and scales, who knows? Instead of trying to re-derive the transform as the current PR does. And hopefully transform_poses_to_original_space()
gets maintained. But it's really really important to be clear about frames etc, and transform_poses_to_original_space()
helps with that a ton.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(note for when we revive this PR, which is planned) for the quaternion multiply if we don't want to deal with the xyzw/wxyz conversion of scipy we can also use (vtf.SO3(wxyz0) @ vtf.SO3(wxyz1)).wxyz
with import viser.transforms as vtf
where viser>=0.1.30
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
voicing a preference for the use of standard scipy / numpy / torch wherever possible
yes it's unfortunate that there are different quaternion encodings, different camera conventions, different euler angle conventions ....
Tough to overcome the inertia here but I agree that this would solve a lot of problems! |
Yes inertia but at least the PR discussions leave breadcrumbs. Things that fall off the rolling katamari ball become seeds for the next re-write. |
Wait, spherical harmonics can easily be rotated by using Wigner matrices, right? |
@jkulhanek Could you share a gist of sample code of rotating spherical harmonics? I am not aware of Wigner matrices. |
Just a note here. Since SH is a complete basis, the rotation is possible exactly. Here is the code: The code is heavy, and I don't understand it fully, but I played with it in a notebook, and it seems to rotate the SH correctly. I can also share the notebook if you want. It's the recursive impl (whatever it means) which is supposed to be more stable (for higher order). The good thing about it is that the wigner matrix can be computed once and then applied to all SHs at once. |
Is there any update on this? I would also be interested in the ability to export splats in their original world frame. I haven't looked into it yet at all but would be willing to contribute. |
@jkulhanek can you provide the notebook? Thanks |
Spherical harmonics functions are equivariant to SO(3) and can be rotated. I don't know the exact code to do it, but probably possible with From ChatGPT import torch
from e3nn.o3 import Irrep, Irreps
from e3nn.o3 import spherical_harmonics, Rotation
R = torch.tensor([ [0.36, 0.48, -0.8], [-0.8, 0.60, 0], [0.48, 0.64, 0.6] ])
l = 2 # Degree of spherical harmonics
directions = torch.tensor([[0.0, 0.0, 1.0]]) # z-axis unit vector
Y_lm = spherical_harmonics(l, directions)
# Get the irreducible representation (Irrep) of the spherical harmonics of degree
l irrep = Irrep(f"{l}e") # e denotes even parity
# Apply the rotation to the spherical harmonics
Y_lm_rotated = irrep.D_from_matrix(R) @ Y_lm Explanation:
|
…brent/splat_export_world_frame
…o-project/nerfstudio into brent/splat_export_world_frame
…brent/splat_export_world_frame
Is Playcanvas related code for SH rotation a good fit to unblock this PR? https://github.com/playcanvas/engine/blob/release-1.69/src/scene/gsplat/gsplat.js#L259 Also, how can we get the matrix that represent the output coordinate? (Or the diff between original and output coordinate) |
The line you cite seems to just load splats? rather than rotate the SHs given an arbitrary transform.
In nerfstudio there's a |
You can use this code https://gist.github.com/jkulhanek/aae3ec12d779ffc729c72157315df0da |
Very helpful explanation, thank you!
Sorry, this is the correct link, which they mentioned |
on first blush it's hard to compare the cited PlayCanvas impl vs @jkulhanek 's link (PlayCanvas looks possibly like an algebraic simplification somehow?). But it would be nice for any final solution to have some unit tests. For example, if dataparser_transform breaks somehow, want to easily determine it's that instead of something in complex sh rotations. |
a739636
to
69d65b5
Compare
The
.ply
export for Gaussians previously defaulted to saving in Nerfstudio's auto-oriented / auto-scaled coordinate frame.I added support for exporting this in the original dataset coordinate frame, which the point cloud export supports via a "Save in world frame" checkbox.
I matched this in both the GUI and the export script CLI:
To me it makes the most sense to default this to
True
(I also flipped this for the point cloud export), but open to thoughts!cc @jb-ye, #2909