12
12
from pytorch3d .structures import Meshes , Pointclouds , join_meshes_as_scene
13
13
14
14
15
+ def get_camera_wireframe (scale : float = 0.3 ):
16
+ """
17
+ Returns a wireframe of a 3D line-plot of a camera symbol.
18
+ """
19
+ a = 0.5 * torch .tensor ([- 2 , 1.5 , 4 ])
20
+ up1 = 0.5 * torch .tensor ([0 , 1.5 , 4 ])
21
+ up2 = 0.5 * torch .tensor ([0 , 2 , 4 ])
22
+ b = 0.5 * torch .tensor ([2 , 1.5 , 4 ])
23
+ c = 0.5 * torch .tensor ([- 2 , - 1.5 , 4 ])
24
+ d = 0.5 * torch .tensor ([2 , - 1.5 , 4 ])
25
+ C = torch .zeros (3 )
26
+ F = torch .tensor ([0 , 0 , 3 ])
27
+ camera_points = [a , up1 , up2 , up1 , b , d , c , a , C , b , d , C , c , C , F ]
28
+ lines = torch .stack ([x .float () for x in camera_points ]) * scale
29
+ return lines
30
+
31
+
15
32
class AxisArgs (NamedTuple ):
16
33
showgrid : bool = False
17
34
zeroline : bool = False
@@ -33,18 +50,20 @@ class Lighting(NamedTuple):
33
50
34
51
35
52
def plot_scene (
36
- plots : Dict [str , Dict [str , Union [Pointclouds , Meshes ]]],
53
+ plots : Dict [str , Dict [str , Union [Pointclouds , Meshes , CamerasBase ]]],
37
54
* ,
38
55
viewpoint_cameras : Optional [CamerasBase ] = None ,
39
56
ncols : int = 1 ,
57
+ camera_scale : float = 0.3 ,
40
58
pointcloud_max_points : int = 20000 ,
41
59
pointcloud_marker_size : int = 1 ,
42
60
** kwargs ,
43
61
):
44
62
"""
45
63
Main function to visualize Meshes and Pointclouds.
46
- Plots input Pointclouds and Meshes data into named subplots,
47
- with named traces based on the dictionary keys.
64
+ Plots input Pointclouds, Meshes, and Cameras data into named subplots,
65
+ with named traces based on the dictionary keys. Cameras are
66
+ rendered at the camera center location using a wireframe.
48
67
49
68
Args:
50
69
plots: A dict containing subplot and trace names,
@@ -57,6 +76,7 @@ def plot_scene(
57
76
for all the subplots will be viewed from that point.
58
77
Otherwise, the viewpoint_cameras will not be used.
59
78
ncols: the number of subplots per row
79
+ camera_scale: determines the size of the wireframe used to render cameras.
60
80
pointcloud_max_points: the maximum number of points to plot from
61
81
a pointcloud. If more are present, a random sample of size
62
82
pointcloud_max_points is used.
@@ -84,7 +104,7 @@ def plot_scene(
84
104
85
105
The above example will render one subplot which has both a mesh and pointcloud.
86
106
87
- If the Meshes or Pointclouds objects are batched, then every object in that batch
107
+ If the Meshes, Pointclouds, or Cameras objects are batched, then every object in that batch
88
108
will be plotted in a single trace.
89
109
90
110
..code-block::python
@@ -144,6 +164,23 @@ def plot_scene(
144
164
The above example will render the first subplot seen from the camera on the +z axis,
145
165
and the second subplot from the viewpoint of the camera on the -z axis.
146
166
167
+ We can visualize these cameras as well:
168
+ ..code-block::python
169
+ mesh = ...
170
+ R, T = look_at_view_transform(2.7, 0, [0, 180]) # 2 camera angles, front and back
171
+ # Any instance of CamerasBase works, here we use FoVPerspectiveCameras
172
+ cameras = FoVPerspectiveCameras(device=device, R=R, T=T)
173
+ fig = plot_scene({
174
+ "subplot1_title": {
175
+ "mesh_trace_title": mesh,
176
+ "cameras_trace_title": cameras,
177
+ },
178
+ })
179
+ fig.show()
180
+
181
+ The above example will render one subplot with the mesh object
182
+ and two cameras.
183
+
147
184
For an example of using kwargs, see below:
148
185
..code-block::python
149
186
mesh = ...
@@ -227,9 +264,15 @@ def plot_scene(
227
264
pointcloud_max_points ,
228
265
pointcloud_marker_size ,
229
266
)
267
+ elif isinstance (struct , CamerasBase ):
268
+ _add_camera_trace (
269
+ fig , struct , trace_name , subplot_idx , ncols , camera_scale
270
+ )
230
271
else :
231
272
raise ValueError (
232
- "struct {} is not a Meshes or Pointclouds object" .format (struct )
273
+ "struct {} is not a Cameras, Meshes or Pointclouds object" .format (
274
+ struct
275
+ )
233
276
)
234
277
235
278
# Ensure update for every subplot.
@@ -285,7 +328,9 @@ def plot_scene(
285
328
286
329
287
330
def plot_batch_individually (
288
- batched_structs : Union [List [Union [Meshes , Pointclouds ]], Meshes , Pointclouds ],
331
+ batched_structs : Union [
332
+ List [Union [Meshes , Pointclouds , CamerasBase ]], Meshes , Pointclouds , CamerasBase
333
+ ],
289
334
* ,
290
335
viewpoint_cameras : Optional [CamerasBase ] = None ,
291
336
ncols : int = 1 ,
@@ -295,26 +340,26 @@ def plot_batch_individually(
295
340
):
296
341
"""
297
342
This is a higher level plotting function than plot_scene, for plotting
298
- Meshes and Pointclouds in simple cases. The simplest use is to plot a
299
- single Meshes or Pointclouds object, where you just pass it in as a
343
+ Cameras, Meshes and Pointclouds in simple cases. The simplest use is to plot a
344
+ single Cameras, Meshes or Pointclouds object, where you just pass it in as a
300
345
one element list. This will plot each batch element in a separate subplot.
301
346
302
- More generally, you can supply multiple Meshes or Pointclouds
347
+ More generally, you can supply multiple Cameras, Meshes or Pointclouds
303
348
having the same batch size `n`. In this case, there will be `n` subplots,
304
349
each depicting the corresponding batch element of all the inputs.
305
350
306
- In addition, you can include Meshes and Pointclouds of size 1 in
351
+ In addition, you can include Cameras, Meshes and Pointclouds of size 1 in
307
352
the input. These will either be rendered in the first subplot
308
353
(if extend_struct is False), or in every subplot.
309
354
310
355
Args:
311
- batched_structs: a list of Meshes and/or Pointclouds to be rendered.
356
+ batched_structs: a list of Cameras, Meshes and/or Pointclouds to be rendered.
312
357
Each structure's corresponding batch element will be plotted in
313
358
a single subplot, resulting in n subplots for a batch of size n.
314
359
Every struct should either have the same batch size or be of batch size 1.
315
360
See extend_struct and the description above for how batch size 1 structs
316
- are handled. Also accepts a single Meshes or Pointclouds object, which will have
317
- each individual element plotted in its own subplot.
361
+ are handled. Also accepts a single Cameras, Meshes or Pointclouds object,
362
+ which will have each individual element plotted in its own subplot.
318
363
viewpoint_cameras: an instance of a Cameras object providing a location
319
364
to view the plotly plot from. If the batch size is equal
320
365
to the number of subplots, it is a one to one mapping.
@@ -408,10 +453,10 @@ def plot_batch_individually(
408
453
409
454
410
455
def _add_struct_from_batch (
411
- batched_struct : Union [Meshes , Pointclouds ],
456
+ batched_struct : Union [CamerasBase , Meshes , Pointclouds ],
412
457
scene_num : int ,
413
458
subplot_title : str ,
414
- scene_dictionary : Dict [str , Dict [str , Union [Meshes , Pointclouds ]]],
459
+ scene_dictionary : Dict [str , Dict [str , Union [CamerasBase , Meshes , Pointclouds ]]],
415
460
trace_idx : int = 1 ,
416
461
):
417
462
"""
@@ -426,8 +471,18 @@ def _add_struct_from_batch(
426
471
scene_dictionary: the dictionary to add the indexed struct to
427
472
trace_idx: the trace number, starting at 1 for this struct's trace
428
473
"""
429
- struct_idx = min (scene_num , len (batched_struct ) - 1 )
430
- struct = batched_struct [struct_idx ]
474
+ struct = None
475
+ if isinstance (batched_struct , CamerasBase ):
476
+ # we can't index directly into camera batches
477
+ R , T = batched_struct .R , batched_struct .T # pyre-ignore[16]
478
+ r_idx = min (scene_num , len (R ) - 1 )
479
+ t_idx = min (scene_num , len (T ) - 1 )
480
+ R = R [r_idx ].unsqueeze (0 )
481
+ T = T [t_idx ].unsqueeze (0 )
482
+ struct = CamerasBase (device = batched_struct .device , R = R , T = T )
483
+ else : # batched meshes and pointclouds are indexable
484
+ struct_idx = min (scene_num , len (batched_struct ) - 1 )
485
+ struct = batched_struct [struct_idx ]
431
486
trace_name = "trace{}-{}" .format (scene_num + 1 , trace_idx )
432
487
scene_dictionary [subplot_title ][trace_name ] = struct
433
488
@@ -568,6 +623,63 @@ def _add_pointcloud_trace(
568
623
_update_axes_bounds (verts_center , max_expand , current_layout )
569
624
570
625
626
+ def _add_camera_trace (
627
+ fig : go .Figure ,
628
+ cameras : CamerasBase ,
629
+ trace_name : str ,
630
+ subplot_idx : int ,
631
+ ncols : int ,
632
+ camera_scale : float ,
633
+ ):
634
+ """
635
+ Adds a trace rendering a Cameras object to the passed in figure, with
636
+ a given name and in a specific subplot.
637
+
638
+ Args:
639
+ fig: plotly figure to add the trace within.
640
+ cameras: the Cameras object to render. It can be batched.
641
+ trace_name: name to label the trace with.
642
+ subplot_idx: identifies the subplot, with 0 being the top left.
643
+ ncols: the number of sublpots per row.
644
+ camera_scale: the size of the wireframe used to render the Cameras object.
645
+ """
646
+ cam_wires = get_camera_wireframe (camera_scale ).to (cameras .device )
647
+ cam_trans = cameras .get_world_to_view_transform ().inverse ()
648
+ cam_wires_trans = cam_trans .transform_points (cam_wires ).detach ().cpu ()
649
+ # if batch size is 1, unsqueeze to add dimension
650
+ if len (cam_wires_trans .shape ) < 3 :
651
+ cam_wires_trans = cam_wires_trans .unsqueeze (0 )
652
+
653
+ nan_tensor = torch .Tensor ([[float ("NaN" )] * 3 ])
654
+ all_cam_wires = cam_wires_trans [0 ]
655
+ for wire in cam_wires_trans [1 :]:
656
+ # We combine camera points into a single tensor to plot them in a
657
+ # single trace. The NaNs are inserted between sets of camera
658
+ # points so that the lines drawn by Plotly are not drawn between
659
+ # points that belong to different cameras.
660
+ all_cam_wires = torch .cat ((all_cam_wires , nan_tensor , wire ))
661
+ x , y , z = all_cam_wires .detach ().cpu ().numpy ().T .astype (float )
662
+
663
+ row , col = subplot_idx // ncols + 1 , subplot_idx % ncols + 1
664
+ fig .add_trace (
665
+ go .Scatter3d ( # pyre-ignore [16]
666
+ x = x , y = y , z = z , marker = {"size" : 1 }, name = trace_name
667
+ ),
668
+ row = row ,
669
+ col = col ,
670
+ )
671
+
672
+ # Access the current subplot's scene configuration
673
+ plot_scene = "scene" + str (subplot_idx + 1 )
674
+ current_layout = fig ["layout" ][plot_scene ]
675
+
676
+ # flatten for bounds calculations
677
+ flattened_wires = cam_wires_trans .flatten (0 , 1 )
678
+ verts_center = flattened_wires .mean (0 )
679
+ max_expand = (flattened_wires .max (0 )[0 ] - flattened_wires .min (0 )[0 ]).max ()
680
+ _update_axes_bounds (verts_center , max_expand , current_layout )
681
+
682
+
571
683
def _gen_fig_with_subplots (batch_size : int , ncols : int , subplot_titles : List [str ]):
572
684
"""
573
685
Takes in the number of objects to be plotted and generate a plotly figure
0 commit comments