|
|
| """ |
| Copyright [2022] [Paul-Edouard Sarlin and Philipp Lindenberger] |
| |
| Licensed under the Apache License, Version 2.0 (the "License"); |
| you may not use this file except in compliance with the License. |
| You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| |
| 3D visualization based on plotly. |
| Works for a small number of points and cameras, might be slow otherwise. |
| |
| 1) Initialize a figure with `init_figure` |
| 2) Add 3D points, camera frustums, or both as a pycolmap.Reconstruction |
| |
| Written by Paul-Edouard Sarlin and Philipp Lindenberger. |
| """ |
| |
|
|
| from typing import Optional |
| import numpy as np |
| import pycolmap |
| import plotly.graph_objects as go |
|
|
|
|
| |
| def qvec2rotmat(qvec): |
| return np.array([ |
| [1 - 2 * qvec[2]**2 - 2 * qvec[3]**2, |
| 2 * qvec[1] * qvec[2] - 2 * qvec[0] * qvec[3], |
| 2 * qvec[3] * qvec[1] + 2 * qvec[0] * qvec[2]], |
| [2 * qvec[1] * qvec[2] + 2 * qvec[0] * qvec[3], |
| 1 - 2 * qvec[1]**2 - 2 * qvec[3]**2, |
| 2 * qvec[2] * qvec[3] - 2 * qvec[0] * qvec[1]], |
| [2 * qvec[3] * qvec[1] - 2 * qvec[0] * qvec[2], |
| 2 * qvec[2] * qvec[3] + 2 * qvec[0] * qvec[1], |
| 1 - 2 * qvec[1]**2 - 2 * qvec[2]**2]]) |
|
|
|
|
| def to_homogeneous(points): |
| pad = np.ones((points.shape[:-1]+(1,)), dtype=points.dtype) |
| return np.concatenate([points, pad], axis=-1) |
|
|
| def t_to_proj_center(qvec, tvec): |
| Rr = qvec2rotmat(qvec) |
| tt = (-Rr.T) @ tvec |
| return tt |
|
|
| def calib(params): |
| out = np.eye(3) |
| if len(params) == 3: |
| out[0,0] = params[0] |
| out[1,1] = params[0] |
| out[0,2] = params[1] |
| out[1,2] = params[2] |
| else: |
| out[0,0] = params[0] |
| out[1,1] = params[1] |
| out[0,2] = params[2] |
| out[1,2] = params[3] |
| return out |
|
|
|
|
| |
|
|
| def init_figure(height: int = 800) -> go.Figure: |
| """Initialize a 3D figure.""" |
| fig = go.Figure() |
| axes = dict( |
| visible=False, |
| showbackground=False, |
| showgrid=False, |
| showline=False, |
| showticklabels=True, |
| autorange=True, |
| ) |
| fig.update_layout( |
| template="plotly_dark", |
| height=height, |
| scene_camera=dict( |
| eye=dict(x=0., y=-.1, z=-2), |
| up=dict(x=0, y=-1., z=0), |
| projection=dict(type="orthographic")), |
| scene=dict( |
| xaxis=axes, |
| yaxis=axes, |
| zaxis=axes, |
| aspectmode='data', |
| dragmode='orbit', |
| ), |
| margin=dict(l=0, r=0, b=0, t=0, pad=0), |
| legend=dict( |
| orientation="h", |
| yanchor="top", |
| y=0.99, |
| xanchor="left", |
| x=0.1 |
| ), |
| ) |
| return fig |
|
|
|
|
| def plot_lines_3d( |
| fig: go.Figure, |
| pts: np.ndarray, |
| color: str = 'rgba(255, 255, 255, 1)', |
| ps: int = 2, |
| colorscale: Optional[str] = None, |
| name: Optional[str] = None): |
| """Plot a set of 3D points.""" |
| x = pts[..., 0] |
| y = pts[..., 1] |
| z = pts[..., 2] |
| traces = [go.Scatter3d(x=x1, y=y1, z=z1, |
| mode='lines', |
| line=dict(color=color, width=2)) for x1, y1, z1 in zip(x,y,z)] |
| for t in traces: |
| fig.add_trace(t) |
| fig.update_traces(showlegend=False) |
|
|
|
|
| def plot_points( |
| fig: go.Figure, |
| pts: np.ndarray, |
| color: str = 'rgba(255, 0, 0, 1)', |
| ps: int = 2, |
| colorscale: Optional[str] = None, |
| name: Optional[str] = None): |
| """Plot a set of 3D points.""" |
| x, y, z = pts.T |
| tr = go.Scatter3d( |
| x=x, y=y, z=z, mode='markers', name=name, legendgroup=name, |
| marker=dict( |
| size=ps, color=color, line_width=0.0, colorscale=colorscale)) |
| fig.add_trace(tr) |
|
|
| def plot_camera( |
| fig: go.Figure, |
| R: np.ndarray, |
| t: np.ndarray, |
| K: np.ndarray, |
| color: str = 'rgb(0, 0, 255)', |
| name: Optional[str] = None, |
| legendgroup: Optional[str] = None, |
| size: float = 1.0): |
| """Plot a camera frustum from pose and intrinsic matrix.""" |
| W, H = K[0, 2]*2, K[1, 2]*2 |
| corners = np.array([[0, 0], [W, 0], [W, H], [0, H], [0, 0]]) |
| if size is not None: |
| image_extent = max(size * W / 1024.0, size * H / 1024.0) |
| world_extent = max(W, H) / (K[0, 0] + K[1, 1]) / 0.5 |
| scale = 0.5 * image_extent / world_extent |
| else: |
| scale = 1.0 |
| corners = to_homogeneous(corners) @ np.linalg.inv(K).T |
| corners = (corners / 2 * scale) @ R.T + t |
|
|
| x, y, z = corners.T |
| rect = go.Scatter3d( |
| x=x, y=y, z=z, line=dict(color=color), legendgroup=legendgroup, |
| name=name, marker=dict(size=0.0001), showlegend=False) |
| fig.add_trace(rect) |
|
|
| x, y, z = np.concatenate(([t], corners)).T |
| i = [0, 0, 0, 0] |
| j = [1, 2, 3, 4] |
| k = [2, 3, 4, 1] |
|
|
| pyramid = go.Mesh3d( |
| x=x, y=y, z=z, color=color, i=i, j=j, k=k, |
| legendgroup=legendgroup, name=name, showlegend=False) |
| fig.add_trace(pyramid) |
| triangles = np.vstack((i, j, k)).T |
| vertices = np.concatenate(([t], corners)) |
| tri_points = np.array([ |
| vertices[i] for i in triangles.reshape(-1) |
| ]) |
|
|
| x, y, z = tri_points.T |
| pyramid = go.Scatter3d( |
| x=x, y=y, z=z, mode='lines', legendgroup=legendgroup, |
| name=name, line=dict(color=color, width=1), showlegend=False) |
| fig.add_trace(pyramid) |
|
|
|
|
| def plot_camera_colmap( |
| fig: go.Figure, |
| image: pycolmap.Image, |
| camera: pycolmap.Camera, |
| name: Optional[str] = None, |
| **kwargs): |
| """Plot a camera frustum from PyCOLMAP objects""" |
| intr = calib(camera.params) |
| if intr[0][0] > 10000: |
| print("Bad camera") |
| return |
| plot_camera( |
| fig, |
| qvec2rotmat(image.qvec).T, |
| t_to_proj_center(image.qvec, image.tvec), |
| intr, |
| name=name or str(image.id), |
| **kwargs) |
|
|
|
|
| def plot_cameras( |
| fig: go.Figure, |
| reconstruction, |
| **kwargs): |
| """Plot a camera as a cone with camera frustum.""" |
| for image_id, image in reconstruction["images"].items(): |
| plot_camera_colmap( |
| fig, image, reconstruction["cameras"][image.camera_id], **kwargs) |
|
|
|
|
| def plot_reconstruction( |
| fig: go.Figure, |
| rec, |
| color: str = 'rgb(0, 0, 255)', |
| name: Optional[str] = None, |
| points: bool = True, |
| cameras: bool = True, |
| cs: float = 1.0, |
| single_color_points=False, |
| camera_color='rgba(0, 255, 0, 0.5)'): |
| |
| |
| xyzs = [] |
| rgbs = [] |
| for k, p3D in rec['points'].items(): |
| xyzs.append(p3D.xyz) |
| rgbs.append(p3D.rgb) |
|
|
| if points: |
| plot_points(fig, np.array(xyzs), color=color if single_color_points else np.array(rgbs), ps=1, name=name) |
| if cameras: |
| plot_cameras(fig, rec, color=camera_color, legendgroup=name, size=cs) |
|
|
|
|
| def plot_pointcloud( |
| fig: go.Figure, |
| pts: np.ndarray, |
| colors: np.ndarray, |
| ps: int = 2, |
| name: Optional[str] = None): |
| """Plot a set of 3D points.""" |
| plot_points(fig, np.array(pts), color=colors, ps=ps, name=name) |
|
|
|
|
| def plot_triangle_mesh( |
| fig: go.Figure, |
| vert: np.ndarray, |
| colors: np.ndarray, |
| triangles: np.ndarray, |
| name: Optional[str] = None): |
| """Plot a triangle mesh.""" |
| tr = go.Mesh3d( |
| x=vert[:,0], |
| y=vert[:,1], |
| z=vert[:,2], |
| vertexcolor = np.clip(255*colors, 0, 255), |
| |
| |
| i=triangles[:,0], |
| j=triangles[:,1], |
| k=triangles[:,2], |
| name=name, |
| showscale=False |
| ) |
| fig.add_trace(tr) |
|
|
| def plot_estimate_and_gt(pred_vertices, pred_connections, gt_vertices=None, gt_connections=None): |
| fig3d = init_figure() |
| c1 = (30, 20, 255) |
| img_color = [c1 for _ in range(len(pred_vertices))] |
| plot_points(fig3d, pred_vertices, color = img_color, ps = 10) |
| lines = [] |
| for c in pred_connections: |
| v1 = pred_vertices[c[0]] |
| v2 = pred_vertices[c[1]] |
| lines.append(np.stack([v1, v2], axis=0)) |
| plot_lines_3d(fig3d, np.array(lines), img_color, ps=4) |
| if gt_vertices is not None: |
| c2 = (30, 255, 20) |
| img_color2 = [c2 for _ in range(len(gt_vertices))] |
| plot_points(fig3d, gt_vertices, color = img_color2, ps = 10) |
| if gt_connections is not None: |
| gt_lines = [] |
| for c in gt_connections: |
| v1 = gt_vertices[c[0]] |
| v2 = gt_vertices[c[1]] |
| gt_lines.append(np.stack([v1, v2], axis=0)) |
| plot_lines_3d(fig3d, np.array(gt_lines), img_color2, ps=4) |
| fig3d.show() |
| return fig3d |
|
|