| |
|
| | """ |
| | 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 |
| |
|