Source code for ouster.sdk.mapping.ply_to_png

import argparse
import logging
import os

import numpy as np
from PIL import Image as PILImage
from plyfile import PlyData  # type: ignore  # noqa

import ouster.sdk.viz as viz

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger('PNG_Maker')

# if beyond max points, random delete points to prevent memory overflow
# 50000000 points is around 30 mins recording with the default PLY maker voxel
# map size. You can increase the voxel map size in OSF save command for
# large maps
max_plot_points = 50000000


[docs]def get_doll_dist(points): # to linearize the plot camera zoon in distance lidar_default_range = 150 min_camera_dist = 2 # bigger number make camera more zoom in camera_magic_n = 20 # because ouster.viz view_distance() uses std::exp(log_distance_ * 0.01) # to linearize the view_distance(). I time 100 for input linearize_n = 100 max_values = np.max(points, axis=0) min_values = np.min(points, axis=0) max_dist = max(((max(abs(max_values - min_values)) - lidar_default_range) / camera_magic_n), min_camera_dist) doll_dist = np.log(max_dist) * linearize_n return doll_dist
[docs]def peak_start_point(ply_files): if not ply_files: logger.error("PLY files list is empty\nExit!") return None # Read the first PLY file file = ply_files[0] try: with open(file, 'rb') as f: ply_data = PlyData.read(f) logger.info("PLY file successfully read.") # Now you can access the data in ply_data except IOError as e: logger.error("Error reading PLY file:", e) # Check if the PLY data is not empty if len(ply_data['vertex']) == 0: logger.error("PLY file is empty\nExit!") return None # Extract the first point as start point x = ply_data['vertex']['x'][0] y = ply_data['vertex']['y'][0] z = ply_data['vertex']['z'][0] logger.info(f"start origin point ({x}, {y}, {z})") return x, y, z
[docs]def output_ext_verify(filename): if not filename.endswith(".png"): raise argparse.ArgumentTypeError("Expect output file end with .png") return filename
[docs]def screenshot_and_save_png(viz, screenshot_path: str): pixels = viz.get_screenshot() PILImage.fromarray(pixels).convert( "RGB").save(screenshot_path) logger.info(f"Screenshot is saved at {screenshot_path}") return screenshot_path
[docs]def scale_rgb_channel(channel): return (channel / (2**8 - 1)).astype(np.float32)
[docs]def convert_ply_to_png(ply_files, screenshot_path, is_plot): start_point = peak_start_point(ply_files) if not start_point: return combined_points = np.empty((0, 3)) combined_keys = None key_field = None for file in ply_files: logger.info(f"processing file : {file}") # Read the PLY file ply_data = PlyData.read(file) x = ply_data['vertex']['x'] - start_point[0] y = ply_data['vertex']['y'] - start_point[1] z = ply_data['vertex']['z'] - start_point[2] points = np.stack((x, y, z), axis=-1) # check if RGB data is available curr_key_field = None if all(channel in ply_data['vertex'].data.dtype.names for channel in ['red', 'green', 'blue']): r = scale_rgb_channel(ply_data['vertex']['red']) g = scale_rgb_channel(ply_data['vertex']['green']) b = scale_rgb_channel(ply_data['vertex']['blue']) key = np.stack((r, g, b), axis=-1) curr_key_field = 'RGB' else: # fallback to other fields possible_keys = ['REFLECTIVITY', 'SIGNAL', 'NEAR_IR'] for key in possible_keys: if key in ply_data['vertex'].data.dtype.names: curr_key_field = key break if curr_key_field is not None: key = ply_data['vertex'][curr_key_field] * 2 if curr_key_field is None: logger.error("No valid key field found in PLY file.") return if key_field is not None and curr_key_field != key_field: logger.error("Inconsistent key fields across PLY files.") return if combined_keys is None: key_field = curr_key_field combined_keys = key combined_points = points else: combined_points = np.concatenate((combined_points, points), axis=0) combined_keys = np.concatenate((combined_keys, key), axis=0) current_size = points.shape[0] if current_size > max_plot_points: logger.info( f"current points {current_size} is greater than" f"maximum {max_plot_points}") points_to_delete = current_size - max_plot_points indices_to_delete = np.random.choice( current_size, points_to_delete, replace=False) points = np.delete(points, indices_to_delete, axis=0) combined_keys = np.delete(combined_keys, indices_to_delete, axis=0) logger.info(f"random delete {points_to_delete} points") doll_dist = get_doll_dist(points) point_viz = viz.PointViz("Viz", True, 3840, 2160) viz.add_default_controls(point_viz) cloud = viz.Cloud(points.shape[0]) cloud.set_xyz(combined_points) if key_field == 'RGB': cloud.set_key_rgb(combined_keys) else: cloud.set_key(combined_keys) cloud.set_palette(viz.magma_palette) point_viz.add(cloud) point_viz.camera.set_dolly(doll_dist) point_viz.update() screenshot_and_save_png(point_viz, screenshot_path) if is_plot: point_viz.run()
if __name__ == "__main__": parser = argparse.ArgumentParser(description="Plot Point Cloud") parser.add_argument( "PLY_files", metavar='file', nargs='+', help="PLY file as input") parser.add_argument( "--dir", help="PNG output dir") parser.add_argument( "--out", type=output_ext_verify, help="PNG output name") parser.add_argument( '--plot', action='store_true', default=False, help='Enable plotting') args = parser.parse_args() file_path = '' if args.dir: if not os.path.exists(args.dir): logger.error(f"The given output dir {args.dir} not exist") exit(1) file_path = args.dir if not file_path.endswith(os.path.sep): file_path += os.path.sep if file_path and not args.out: logger.error(f"Given the output dir {file_path} without the output " f"filename {args.out}. Use --out to specify the out filename") exit(1) if args.out: file_path += args.out convert_ply_to_png(args.PLY_files, file_path, args.plot)