mirror of
https://github.com/facebookresearch/pytorch3d.git
synced 2025-08-02 20:02:49 +08:00
Render plotly plot from PyTorch3D renderer POV
Summary: Use a provided renderer's camera positions to render a plotly plot to match what the renderer would render for pointclouds and meshes. - takes in a Cameras object for viewpoints - for each subplot, will index into the Cameras object (or use the Cameras object, if len(viewpoint_cameras) == 1 and use the Cameras' eye and at vectors to set plotly's camera's corresponding values, the eye and center values. Reviewed By: nikhilaravi Differential Revision: D24094934 fbshipit-source-id: 48abcdb04c6909a172ba9f721522c3446952a089
This commit is contained in:
parent
3d1863ce6f
commit
035109675e
@ -1,13 +1,14 @@
|
|||||||
# Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
|
# Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
|
||||||
|
|
||||||
import warnings
|
import warnings
|
||||||
from typing import Dict, List, NamedTuple, Optional, Union
|
from typing import Dict, List, NamedTuple, Optional, Tuple, Union
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import plotly.graph_objects as go
|
import plotly.graph_objects as go
|
||||||
import torch
|
import torch
|
||||||
from plotly.subplots import make_subplots
|
from plotly.subplots import make_subplots
|
||||||
from pytorch3d.renderer import TexturesVertex
|
from pytorch3d.renderer import TexturesVertex
|
||||||
|
from pytorch3d.renderer.cameras import CamerasBase
|
||||||
from pytorch3d.structures import Meshes, Pointclouds, join_meshes_as_scene
|
from pytorch3d.structures import Meshes, Pointclouds, join_meshes_as_scene
|
||||||
|
|
||||||
|
|
||||||
@ -34,6 +35,7 @@ class Lighting(NamedTuple):
|
|||||||
def plot_scene(
|
def plot_scene(
|
||||||
plots: Dict[str, Dict[str, Union[Pointclouds, Meshes]]],
|
plots: Dict[str, Dict[str, Union[Pointclouds, Meshes]]],
|
||||||
*,
|
*,
|
||||||
|
viewpoint_cameras: Optional[CamerasBase] = None,
|
||||||
ncols: int = 1,
|
ncols: int = 1,
|
||||||
pointcloud_max_points: int = 20000,
|
pointcloud_max_points: int = 20000,
|
||||||
pointcloud_marker_size: int = 1,
|
pointcloud_marker_size: int = 1,
|
||||||
@ -48,6 +50,12 @@ def plot_scene(
|
|||||||
plots: A dict containing subplot and trace names,
|
plots: A dict containing subplot and trace names,
|
||||||
as well as the Meshes and Pointclouds objects to be rendered.
|
as well as the Meshes and Pointclouds objects to be rendered.
|
||||||
See below for examples of the format.
|
See below for examples of the format.
|
||||||
|
viewpoint_cameras: an instance of a Cameras object providing a location
|
||||||
|
to view the plotly plot from. If the batch size is equal
|
||||||
|
to the number of subplots, it is a one to one mapping.
|
||||||
|
If the batch size is 1, then that viewpoint will be used
|
||||||
|
for all the subplots will be viewed from that point.
|
||||||
|
Otherwise, the viewpoint_cameras will not be used.
|
||||||
ncols: the number of subplots per row
|
ncols: the number of subplots per row
|
||||||
pointcloud_max_points: the maximum number of points to plot from
|
pointcloud_max_points: the maximum number of points to plot from
|
||||||
a pointcloud. If more are present, a random sample of size
|
a pointcloud. If more are present, a random sample of size
|
||||||
@ -115,6 +123,27 @@ def plot_scene(
|
|||||||
instead of having them vertically stacked because the default is one subplot
|
instead of having them vertically stacked because the default is one subplot
|
||||||
per row.
|
per row.
|
||||||
|
|
||||||
|
To view plotly plots from a PyTorch3D camera's point of view, we can use
|
||||||
|
viewpoint_cameras:
|
||||||
|
..code-block::python
|
||||||
|
mesh = ... # batch size 2
|
||||||
|
R, T = look_at_view_transform(2.7, 0, [0, 180]) # 2 camera angles, front and back
|
||||||
|
# Any instance of CamerasBase works, here we use FoVPerspectiveCameras
|
||||||
|
cameras = FoVPerspectiveCameras(device=device, R=R, T=T)
|
||||||
|
fig = plot_scene({
|
||||||
|
"subplot1_title": {
|
||||||
|
"mesh_trace_title": mesh[0]
|
||||||
|
},
|
||||||
|
"subplot2_title": {
|
||||||
|
"mesh_trace_title": mesh[1]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
viewpoint_cameras=cameras)
|
||||||
|
fig.show()
|
||||||
|
|
||||||
|
The above example will render the first subplot seen from the camera on the +z axis,
|
||||||
|
and the second subplot from the viewpoint of the camera on the -z axis.
|
||||||
|
|
||||||
For an example of using kwargs, see below:
|
For an example of using kwargs, see below:
|
||||||
..code-block::python
|
..code-block::python
|
||||||
mesh = ...
|
mesh = ...
|
||||||
@ -133,6 +162,7 @@ def plot_scene(
|
|||||||
See the tutorials in pytorch3d/docs/tutorials for more examples
|
See the tutorials in pytorch3d/docs/tutorials for more examples
|
||||||
(namely rendered_color_points.ipynb and rendered_textured_meshes.ipynb).
|
(namely rendered_color_points.ipynb and rendered_textured_meshes.ipynb).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
subplots = list(plots.keys())
|
subplots = list(plots.keys())
|
||||||
fig = _gen_fig_with_subplots(len(subplots), ncols, subplots)
|
fig = _gen_fig_with_subplots(len(subplots), ncols, subplots)
|
||||||
lighting = kwargs.get("lighting", Lighting())._asdict()
|
lighting = kwargs.get("lighting", Lighting())._asdict()
|
||||||
@ -155,6 +185,31 @@ def plot_scene(
|
|||||||
"z": 0,
|
"z": 0,
|
||||||
} # set the up vector to match PyTorch3D world coordinates conventions
|
} # set the up vector to match PyTorch3D world coordinates conventions
|
||||||
}
|
}
|
||||||
|
viewpoints_eye_at_up_world = None
|
||||||
|
if viewpoint_cameras:
|
||||||
|
if len(viewpoint_cameras) == len(subplots) or len(viewpoint_cameras) == 1:
|
||||||
|
# Calculate the vectors eye, at, up in world space
|
||||||
|
# to initialize the position of the camera in
|
||||||
|
# the plotly figure
|
||||||
|
# TODO(T77879494): correct the up vector calculation in the world space.
|
||||||
|
eye_at_up_view = (
|
||||||
|
torch.tensor([[0, 0, 0], [0, 0, 1], [0, 1, 0]])
|
||||||
|
.float()
|
||||||
|
.to(viewpoint_cameras.device)
|
||||||
|
)
|
||||||
|
viewpoints_eye_at_up_world = (
|
||||||
|
viewpoint_cameras.get_world_to_view_transform()
|
||||||
|
.inverse()
|
||||||
|
.transform_points(eye_at_up_view)
|
||||||
|
)
|
||||||
|
if len(viewpoints_eye_at_up_world.shape) < 3:
|
||||||
|
viewpoints_eye_at_up_world = viewpoints_eye_at_up_world.unsqueeze(0)
|
||||||
|
else:
|
||||||
|
msg = "Invalid number {} of viewpoint cameras were provided. Either 1 \
|
||||||
|
or {} cameras are required".format(
|
||||||
|
len(viewpoint_cameras), len(subplots)
|
||||||
|
)
|
||||||
|
warnings.warn(msg)
|
||||||
|
|
||||||
for subplot_idx in range(len(subplots)):
|
for subplot_idx in range(len(subplots)):
|
||||||
subplot_name = subplots[subplot_idx]
|
subplot_name = subplots[subplot_idx]
|
||||||
@ -189,6 +244,33 @@ def plot_scene(
|
|||||||
yaxis.update(**y_settings)
|
yaxis.update(**y_settings)
|
||||||
zaxis.update(**z_settings)
|
zaxis.update(**z_settings)
|
||||||
|
|
||||||
|
# update camera viewpoint if provided
|
||||||
|
if viewpoints_eye_at_up_world is not None:
|
||||||
|
# Use camera params for batch index or the first camera if only one provided.
|
||||||
|
viewpoint_idx = min(len(viewpoints_eye_at_up_world) - 1, subplot_idx)
|
||||||
|
|
||||||
|
eye, at, _up = viewpoints_eye_at_up_world[viewpoint_idx]
|
||||||
|
|
||||||
|
eye_x, eye_y, eye_z = eye.tolist()
|
||||||
|
|
||||||
|
at_x, at_y, at_z = at.tolist()
|
||||||
|
|
||||||
|
# scale camera eye to plotly [-1, 1] ranges
|
||||||
|
x_range = xaxis["range"]
|
||||||
|
y_range = yaxis["range"]
|
||||||
|
z_range = zaxis["range"]
|
||||||
|
|
||||||
|
eye_x = _scale_camera_to_bounds(eye_x, x_range)
|
||||||
|
eye_y = _scale_camera_to_bounds(eye_y, y_range)
|
||||||
|
eye_z = _scale_camera_to_bounds(eye_z, z_range)
|
||||||
|
|
||||||
|
at_x = _scale_camera_to_bounds(at_x, x_range)
|
||||||
|
at_y = _scale_camera_to_bounds(at_y, y_range)
|
||||||
|
at_z = _scale_camera_to_bounds(at_z, z_range)
|
||||||
|
|
||||||
|
camera["eye"] = {"x": eye_x, "y": eye_y, "z": eye_z}
|
||||||
|
camera["center"] = {"x": at_x, "y": at_y, "z": at_z}
|
||||||
|
|
||||||
current_layout.update(
|
current_layout.update(
|
||||||
{
|
{
|
||||||
"xaxis": xaxis,
|
"xaxis": xaxis,
|
||||||
@ -205,6 +287,7 @@ def plot_scene(
|
|||||||
def plot_batch_individually(
|
def plot_batch_individually(
|
||||||
batched_structs: Union[List[Union[Meshes, Pointclouds]], Meshes, Pointclouds],
|
batched_structs: Union[List[Union[Meshes, Pointclouds]], Meshes, Pointclouds],
|
||||||
*,
|
*,
|
||||||
|
viewpoint_cameras: Optional[CamerasBase] = None,
|
||||||
ncols: int = 1,
|
ncols: int = 1,
|
||||||
extend_struct: bool = True,
|
extend_struct: bool = True,
|
||||||
subplot_titles: Optional[List[str]] = None,
|
subplot_titles: Optional[List[str]] = None,
|
||||||
@ -232,6 +315,12 @@ def plot_batch_individually(
|
|||||||
See extend_struct and the description above for how batch size 1 structs
|
See extend_struct and the description above for how batch size 1 structs
|
||||||
are handled. Also accepts a single Meshes or Pointclouds object, which will have
|
are handled. Also accepts a single Meshes or Pointclouds object, which will have
|
||||||
each individual element plotted in its own subplot.
|
each individual element plotted in its own subplot.
|
||||||
|
viewpoint_cameras: an instance of a Cameras object providing a location
|
||||||
|
to view the plotly plot from. If the batch size is equal
|
||||||
|
to the number of subplots, it is a one to one mapping.
|
||||||
|
If the batch size is 1, then that viewpoint will be used
|
||||||
|
for all the subplots will be viewed from that point.
|
||||||
|
Otherwise, the viewpoint_cameras will not be used.
|
||||||
ncols: the number of subplots per row
|
ncols: the number of subplots per row
|
||||||
extend_struct: if True, indicates that structs of batch size 1
|
extend_struct: if True, indicates that structs of batch size 1
|
||||||
should be plotted in every subplot.
|
should be plotted in every subplot.
|
||||||
@ -313,7 +402,9 @@ def plot_batch_individually(
|
|||||||
batched_structs, scene_num, subplot_title, scene_dictionary
|
batched_structs, scene_num, subplot_title, scene_dictionary
|
||||||
)
|
)
|
||||||
|
|
||||||
return plot_scene(scene_dictionary, ncols=ncols, **kwargs)
|
return plot_scene(
|
||||||
|
scene_dictionary, viewpoint_cameras=viewpoint_cameras, ncols=ncols, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _add_struct_from_batch(
|
def _add_struct_from_batch(
|
||||||
@ -545,3 +636,20 @@ def _update_axes_bounds(
|
|||||||
yaxis = {"range": y_range}
|
yaxis = {"range": y_range}
|
||||||
zaxis = {"range": z_range}
|
zaxis = {"range": z_range}
|
||||||
current_layout.update({"xaxis": xaxis, "yaxis": yaxis, "zaxis": zaxis})
|
current_layout.update({"xaxis": xaxis, "yaxis": yaxis, "zaxis": zaxis})
|
||||||
|
|
||||||
|
|
||||||
|
def _scale_camera_to_bounds(coordinate: float, axis_bounds: Tuple[float, float]):
|
||||||
|
"""
|
||||||
|
We set our plotly plot's axes' bounding box to [-1,1]x[-1,1]x[-1,1]. As such,
|
||||||
|
the plotly camera location has to be scaled accordingly to have its world coordinates
|
||||||
|
correspond to its relative plotted coordinates for viewing the plotly plot.
|
||||||
|
This function does the scaling and offset to transform the coordinates.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
coordinate: the float value to be transformed
|
||||||
|
axis_bounds: the bounds of the plotly plot for the axis which
|
||||||
|
the coordinate argument refers to
|
||||||
|
"""
|
||||||
|
scale = (axis_bounds[1] - axis_bounds[0]) / 2
|
||||||
|
offset = (axis_bounds[1] / scale) - 1
|
||||||
|
return coordinate / scale - offset
|
||||||
|
Loading…
x
Reference in New Issue
Block a user