SplatAD: render lidar through get_outputs_for_lidar (fixes #75, #79) - #93
YuvrajPuyam wants to merge 4 commits into
Conversation
…rghess#79) SplatADModel inherited ADModel.get_outputs_for_lidar, which builds a RayBundle and calls forward(ray_bundle=...). SplatAD only knows Cameras and Lidars, so rendering lidar from a trained checkpoint (render.py, viewer) died with "Unknown sensor type". Add a rasterization-based get_outputs_for_lidar on SplatADModel. render.py hands over raw dataset entries, so the method builds raster_pts itself when they are missing, reusing the datamanager's raster code (now static so the model can call it). Returns the rendered points in the lidar frame alongside ray_drop_prob, plus a validity mask that render.py now applies. Checked on PandaSet 001: render.py used to crash at 0/40 lidars, now renders a scan in ~0.25 s; median range error vs the real sweep 3.5 cm on a 1k-iteration checkpoint.
The viewer's _render_lidar built a RayBundle, so it hit the same "Unknown sensor type" as render.py. For models that set renders_lidar_by_rasterization, build a Lidars on the panel's grid (beams, FoV, azimuth resolution, position) and call get_outputs_for_lidar; ray-based models keep the RayBundle path. Ray-drop and distance-cutoff filtering unchanged. The grid branch now lays cells out like the datamanager does (azimuth ascending from -180, padded to whole tiles, padded cells masked out) and gives the virtual sensor zero velocities for the rolling-shutter code. Lidars never registered azimuths/elevations as [*num_lidars, n, 1] fields, so constructing one with a grid failed to broadcast; add them to _field_custom_dimensions.
|
Numbers from a 30k-iteration checkpoint are in (PandaSet 001, 10 eval scans, same script as before): median range error 1.3 cm (1.2-1.5 across scans), ray-drop accuracy 96.7%, intensity RMSE 0.083, about 0.3 s per scan on an A30. That lines up with the paper's fully-trained figures, so the rendering path matches what training sees. Updated the description and swapped the BEV plot to this checkpoint. The viewer path on the same checkpoint, rendered headlessly with the panel defaults (24 beams, 0.5 deg): |
|
Same check across all ten SplatAD PandaSet sequences now (30k iterations each, 10 eval scans per scene, 100 scans total): median range error 1.2 cm, ray-drop accuracy 96.7%, intensity RMSE 0.061. Per scene:
Updated the description with the aggregate. Script that produced these is the same one as before (renders through |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2a564e6bb7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| )[None] | ||
| boundaries = elevations[::ELEV_CHANNELS_PER_TILE] | ||
| lidar.metadata["raster_pts"] = raster_pts | ||
| lidar.metadata["elevation_boundaries"] = torch.cat([boundaries, boundaries[-1:] + 1.0]) |
There was a problem hiding this comment.
Bracket the final elevation tile
When the viewer uses its default 24 beams over -15° to 15°, boundaries contains elevations 0, 8, and 16, so this sets the maximum boundary to about 6.87° even though seven valid beams extend through 15°. get_lidar_outputs passes that value as max_elevation and as the final tile boundary, causing the upper portion of the requested vertical FoV to be omitted or assigned incorrectly. Construct boundaries around every eight-channel group, with the final boundary above elevations[-1], as get_lidar_raster_params does.
Useful? React with 👍 / 👎.
The grid branch built tile boundaries as elevations[::8] plus one degree above the last tile start, so a 24-beam grid over -15..15 deg ended at 6.87 deg and lost the top tiles. Use the same construction as get_lidar_raster_params (first minus one, group midpoints, last plus one), factored into grid_tile_elevation_boundaries with a unit test.
|
Good catch, that was a real bug in the grid branch: I built the tile boundaries as |
1264569 to
16726e2
Compare
16726e2 to
7b1882d
Compare
|
@georghess Codex comment is addressed (the grid branch was not bracketing the last elevation tile, fixed with a helper and a unit test), branch is up to date with main, tests and ruff clean at the pinned 0.1.13. The red check is not from this PR: Core Tests fails the same way on main, the second |

Rendering lidar from a trained SplatAD checkpoint fails with
ValueError: Unknown sensor type(#75, #79). It's been open a while and there are a couple of partial snippets in the thread, so here's a proper fix.What's going on
SplatADModelinheritsget_outputs_for_lidarfromADModel. That implementation is for the ray-based models: it turns the scan into aRayBundleand callsforward(ray_bundle=...), andSplatADModel.get_outputsonly acceptsCameras/Lidars, so it raises. Bothrender.py(dataset --render-point-clouds True) and the viewer's_render_lidargo through that path.The snippet in the thread (a
get_outputs_for_lidarthat returnsget_outputs(lidar)) gets you past the exception but not much further:render.pyexpects(outputs, batch)with a point cloud inoutputs["points"], and since it reads raw dataset entries rather than the datamanager's cached ones, there is noraster_ptsforget_lidar_outputsto rasterize.The change
SplatADModel.get_outputs_for_lidar(lidar, batch=None): buildsraster_ptsfrombatch["lidar"]when it isn't there (using the datamanager's own raster code), uses a precomputed one if present, or falls back to the sensor's azimuth/elevation grid if no scan is given. Returns(outputs, batch);outputs["points"]is[B, H, W, 3]in the lidar frame, same layout asray_drop_prob, plusoutputs["points_valid"]for cells that had a real return.full_images_lidar_datamanager.py:_lidar_to_raster_ptsbecomes a staticmethod and the body of_add_metadatamoves into a staticadd_raster_metadata(lidar, data, device)so the model can call it. The per-lidar-type elevation/azimuth setup in_load_lidarsis pulled out intoget_lidar_raster_params. No behavior change for training.render.py: also mask bypoints_validwhen it's present (NeuRAD unaffected).LidarType.Checked
PandaSet 001, SplatAD trained 30k iterations, A30. Same checkpoint before and after:
render.py dataset --render-point-clouds Truecrashed at 0/40 lidars on main, with this it renders (~0.3 s per scan). Against the real sweeps on the 10 eval scans, using the model's own eval formulas: median range error 1.3 cm (1.2-1.5 across scans), ray-drop accuracy 96.7%, intensity RMSE 0.083. Across all ten SplatAD PandaSet sequences (30k iterations each, 100 eval scans total): median range error 1.2 cm (0.2-2.2 per scene), ray-drop accuracy 96.7%, intensity RMSE 0.061; per-scene table in the comments. That's in line with what the paper reports for fully trained scenes (1-2 cm, 92.6-96.7%), so the rendering path is doing what training was doing. Real vs rendered, BEV:Viewer
The viewer's
_render_lidarhad the same problem (it builds a RayBundle from the control panel). @xdtyjwj's snippet in #75 showed that handing the model aLidarsgrid works there, so the second commit does that properly: for models that setrenders_lidar_by_rasterization,_render_lidarbuilds aLidarsfrom the panel's actual beams / FoV / azimuth resolution / position and callsget_outputs_for_lidar; ray-based models keep the RayBundle path unchanged. The ray-drop and distance-cutoff filters stay in place. The grid branch lays cells out the way the datamanager does (azimuth ascending from -180, padded to whole tiles, padded cells masked out) and gives the virtual sensor zero velocities so the rolling-shutter code is happy. One small thing surfaced on the way:Lidarsnever registeredazimuths/elevationsas[*num_lidars, n, 1]fields in_field_custom_dimensions, so constructing one with a grid failed to broadcast (I suspect that's what pushed the thread's workaround into editing lidars.py); fixed by registering them. I checked it headlessly against a checkpoint with the viewer defaults (24 beams, 0.5 deg) and an odd size that needs padding (30 beams, 0.3 deg); I haven't clicked through the actual viser UI on the cluster, so a quick look from someone with a display would be welcome.Unrelated but you'll hit it right after this:
plot_lidar_pointsin render.py needs kaleido>=1 (and a Chrome install) with plotly 7. Left alone here.Fixes #75, fixes #79.