from pathlib import Path from sys import exit import matplotlib.pyplot as plt import numpy as np from configargparse import ( ArgParser, ArgumentDefaultsRawHelpFormatter, YAMLConfigFileParser, ) from open3d.io import read_pinhole_camera_parameters, write_image from open3d.utility import Vector3dVector from open3d.visualization.rendering import MaterialRecord, OffscreenRenderer from pointcloudset import Dataset from rich.progress import track from util import ( calculate_average_frame_rate, create_video_from_images, existing_file, load_dataset, ) def render_3d_images( dataset: Dataset, camera_config_input_json_path: Path, output_images_path: Path, image_pattern_prefix, ) -> list[Path]: camera_params = read_pinhole_camera_parameters("saved_camera_settings.json") width, height = 1920, 1080 renderer = OffscreenRenderer(width, height) renderer.setup_camera( intrinsic_matrix=camera_params.intrinsic.intrinsic_matrix, extrinsic_matrix=camera_params.extrinsic, intrinsic_height_px=camera_params.intrinsic.height, intrinsic_width_px=camera_params.intrinsic.width, ) renderer.scene.set_background([1, 1, 1, 1]) def color_points_by_range(pcd): points = np.asarray(pcd.points) distances = np.linalg.norm(points, axis=1) max_distance = distances.max() min_distance = distances.min() normalized_distances = (distances - min_distance) / ( max_distance - min_distance ) colors = plt.get_cmap("jet")(normalized_distances)[:, :3] pcd.colors = Vector3dVector(colors) return pcd rendered_images = [] for i, pc in track( enumerate(dataset, 1), description="Rendering images...", total=len(dataset) ): o3d_pc = pc.to_instance("open3d") o3d_pc = color_points_by_range(o3d_pc) renderer.scene.add_geometry("point_cloud", o3d_pc, MaterialRecord()) image_path = output_images_path / f"{image_pattern_prefix}_{i:04d}.png" write_image(image_path.as_posix(), renderer.render_to_image()) renderer.scene.remove_geometry("point_cloud") rendered_images.append(image_path) return rendered_images def main() -> int: parser = ArgParser( config_file_parser_class=YAMLConfigFileParser, default_config_files=["render3d_config.yaml"], formatter_class=ArgumentDefaultsRawHelpFormatter, description="Render a 3d representation of a point cloud", ) parser.add_argument( "--render-config-file", is_config_file=True, help="yaml config file path" ) parser.add_argument( "--input-bag-path", required=True, type=existing_file, help="path to bag file" ) parser.add_argument( "--tmp-files-path", default=Path("./tmp"), type=Path, help="path temporary files will be written to", ) parser.add_argument( "--output-images", type=bool, default=True, help="if rendered frames should be outputted as images", ) parser.add_argument( "--output-images-path", default=Path("./output"), type=Path, help="path rendered frames should be written to", ) parser.add_argument( "--output-video", type=bool, default=True, help="if rendered frames should be outputted as a video", ) parser.add_argument( "--output-video-path", default=Path("./output/2d_render.mp4"), type=Path, help="path rendered video should be written to", ) parser.add_argument( "--output-images-prefix", default="2d_render", type=str, help="filename prefix for output", ) parser.add_argument( "--camera-config-input-json-path", default="./saved_camera_settings.json", type=existing_file, help="path to json file containing camera settings (can be created with the create_camera_settings.py script)", ) args = parser.parse_args() if args.output_images: args.output_images_path.mkdir(parents=True, exist_ok=True) args.tmp_files_path = args.output_images_path else: args.tmp_files_path.mkdir(parents=True, exist_ok=True) if args.output_video: args.output_video_path.parent.mkdir(parents=True, exist_ok=True) dataset = load_dataset(args.input_bag_path) images = render_3d_images( dataset, args.camera_config_input_json_path, args.tmp_files_path, args.output_images_prefix, ) if args.output_video: input_images_pattern = ( f"{args.tmp_files_path / args.output_images_prefix}_%04d.png" ) create_video_from_images( input_images_pattern, args.output_video_path, calculate_average_frame_rate(dataset), ) if not args.output_images: for image in images: image.unlink() return 0 if __name__ == "__main__": exit(main())