.. Comment: this file is automatically generated by `update_example_docs.py`. It should not be modified manually. Plots as Images ========================================== Examples of sending plots as images to Viser's GUI panel. This can be faster than using Plotly. .. code-block:: python :linenos: import colorsys import time import cv2 import numpy as np import tyro import viser import viser.transforms as vtf def get_line_plot( xs: np.ndarray, ys: np.ndarray, height: int, width: int, *, x_bounds: tuple[float, float] | None = None, y_bounds: tuple[float, float] | None = None, title: str | None = None, line_thickness: int = 2, grid_x_lines: int = 8, grid_y_lines: int = 5, font_scale: float = 0.4, background_color: tuple[int, int, int] = (0, 0, 0), plot_area_color: tuple[int, int, int] = (0, 0, 0), grid_color: tuple[int, int, int] = (60, 60, 60), axes_color: tuple[int, int, int] = (100, 100, 100), line_color: tuple[int, int, int] = (255, 255, 255), text_color: tuple[int, int, int] = (200, 200, 200), ) -> np.ndarray: """Create a line plot using OpenCV with axes, labels, and grid. This is much faster than using libraries like Matplotlib or Plotly, but is less flexible. """ if x_bounds is None: x_bounds = (np.min(xs), np.max(xs.round(decimals=4))) if y_bounds is None: y_bounds = (np.min(ys), np.max(ys)) # Calculate text sizes for padding. font = cv2.FONT_HERSHEY_DUPLEX sample_y_label = f"{max(abs(y_bounds[0]), abs(y_bounds[1])):.1f}" y_text_size = cv2.getTextSize(sample_y_label, font, font_scale, 1)[0] sample_x_label = f"{max(abs(x_bounds[0]), abs(x_bounds[1])):.1f}" x_text_size = cv2.getTextSize(sample_x_label, font, font_scale, 1)[0] # Define padding based on font scale. extra_padding = 8 left_pad = int(y_text_size[0] * 1.5) + extra_padding # Space for y-axis labels right_pad = int(10 * font_scale) + extra_padding # Calculate top padding, accounting for title if present top_pad = int(10 * font_scale) + extra_padding title_font_scale = font_scale * 1.5 # Make title slightly larger if title is not None: title_size = cv2.getTextSize(title, font, title_font_scale, 1)[0] top_pad += title_size[1] + int(10 * font_scale) bottom_pad = int(x_text_size[1] * 2.0) + extra_padding # Space for x-axis labels # Create larger image to accommodate padding. total_height = height total_width = width plot_width = width - left_pad - right_pad plot_height = height - top_pad - bottom_pad assert plot_width > 0 and plot_height > 0 # Create image with specified background color img = np.ones((total_height, total_width, 3), dtype=np.uint8) img[:] = background_color # Create plot area with specified color plot_area = np.ones((plot_height, plot_width, 3), dtype=np.uint8) plot_area[:] = plot_area_color img[top_pad : top_pad + plot_height, left_pad : left_pad + plot_width] = plot_area def scale_to_pixels(values, bounds, pixels): """Scale values from bounds range to pixel coordinates.""" min_val, max_val = bounds normalized = (values - min_val) / (max_val - min_val) return (normalized * (pixels - 1)).astype(np.int32) # Vertical grid lines. for i in range(grid_x_lines): x_pos = left_pad + int(plot_width * i / (grid_x_lines - 1)) cv2.line(img, (x_pos, top_pad), (x_pos, top_pad + plot_height), grid_color, 1) # Horizontal grid lines. for i in range(grid_y_lines): y_pos = top_pad + int(plot_height * i / (grid_y_lines - 1)) cv2.line(img, (left_pad, y_pos), (left_pad + plot_width, y_pos), grid_color, 1) # Draw axes. cv2.line( img, (left_pad, top_pad + plot_height), (left_pad + plot_width, top_pad + plot_height), axes_color, 1, ) # x-axis cv2.line( img, (left_pad, top_pad), (left_pad, top_pad + plot_height), axes_color, 1 ) # y-axis # Scale and plot the data. x_scaled = scale_to_pixels(xs, x_bounds, plot_width) + left_pad y_scaled = top_pad + plot_height - 1 - scale_to_pixels(ys, y_bounds, plot_height) pts = np.column_stack((x_scaled, y_scaled)).reshape((-1, 1, 2)) # Draw the main plot line. cv2.polylines( img, [pts], False, line_color, thickness=line_thickness, lineType=cv2.LINE_AA ) # Draw title if specified if title is not None: title_size = cv2.getTextSize(title, font, title_font_scale, 1)[0] title_x = left_pad + (plot_width - title_size[0]) // 2 title_y = int(top_pad / 2) + title_size[1] // 2 - 1 cv2.putText( img, title, (title_x, title_y), font, title_font_scale, text_color, 1, cv2.LINE_AA, ) # X-axis labels. for i in range(grid_x_lines): x_val = x_bounds[0] + (x_bounds[1] - x_bounds[0]) * i / (grid_x_lines - 1) x_pos = left_pad + int(plot_width * i / (grid_x_lines - 1)) label = f"{x_val:.1f}" if label == "-0.0": label = "0.0" text_size = cv2.getTextSize(label, font, font_scale, 1)[0] cv2.putText( img, label, (x_pos - text_size[0] // 2, top_pad + plot_height + text_size[1] + 10), font, font_scale, text_color, 1, cv2.LINE_AA, ) # Y-axis labels. for i in range(grid_y_lines): y_val = y_bounds[0] + (y_bounds[1] - y_bounds[0]) * (grid_y_lines - 1 - i) / ( grid_y_lines - 1 ) y_pos = top_pad + int(plot_height * i / (grid_y_lines - 1)) label = f"{y_val:.1f}" if label == "-0.0": label = "0.0" text_size = cv2.getTextSize(label, font, font_scale, 1)[0] cv2.putText( img, label, (left_pad - text_size[0] - 5, y_pos + 5), font, font_scale, text_color, 1, cv2.LINE_AA, ) return img def create_sine_plot(title: str, counter: int) -> np.ndarray: """Create a sine wave plot with the given counter offset.""" xs = np.linspace(0, 2 * np.pi, 20) rgb = colorsys.hsv_to_rgb(counter / 4000 % 1, 1, 1) return get_line_plot( xs=xs, ys=np.sin(xs + counter / 20), title=title, line_color=(int(rgb[0] * 255), int(rgb[1] * 255), int(rgb[2] * 255)), height=150, width=350, ) def main(num_plots: int = 8) -> None: server = viser.ViserServer() # Create GUI elements for display runtimes. with server.gui.add_folder("Runtime"): draw_time = server.gui.add_text("Draw / plot (ms)", "0.00", disabled=True) send_gui_time = server.gui.add_text( "Gui update / plot (ms)", "0.00", disabled=True ) send_scene_time = server.gui.add_text( "Scene update / plot (ms)", "0.00", disabled=True ) # Add 2D plots to the GUI. with server.gui.add_folder("Plots"): plots_cb = server.gui.add_checkbox("Update plots", True) gui_image_handles = [ server.gui.add_image( create_sine_plot(f"Plot {i}", counter=0), label=f"Image {i}", format="jpeg", ) for i in range(num_plots) ] # Add 2D plots to the scene. We flip them with a parent coordinate frame. server.scene.add_frame( "/images", wxyz=vtf.SO3.from_y_radians(np.pi).wxyz, show_axes=False ) scene_image_handles = [ server.scene.add_image( f"/images/plot{i}", image=gui_image_handles[i].image, render_width=3.5, render_height=1.5, format="jpeg", position=( (i % 2 - 0.5) * 3.5, (i // 2 - (num_plots - 1) / 4) * 1.5, 0, ), ) for i in range(num_plots) ] counter = 0 while True: if plots_cb.value: # Create and time the plot generation. start = time.time() images = [ create_sine_plot(f"Plot {i}", counter=counter * (i + 1)) for i in range(num_plots) ] draw_time.value = f"{0.98 * float(draw_time.value) + 0.02 * (time.time() - start) / num_plots * 1000:.2f}" # Update all plot images. start = time.time() for i, handle in enumerate(gui_image_handles): handle.image = images[i] send_gui_time.value = f"{0.98 * float(send_gui_time.value) + 0.02 * (time.time() - start) / num_plots * 1000:.2f}" # Update all scene images. start = time.time() for i, handle in enumerate(scene_image_handles): handle.image = gui_image_handles[i].image send_scene_time.value = f"{0.98 * float(send_scene_time.value) + 0.02 * (time.time() - start) / num_plots * 1000:.2f}" # Sleep a bit before continuing. time.sleep(0.02) counter += 1 if __name__ == "__main__": tyro.cli(main)