| import gradio as gr |
| import numpy as np |
| import laspy |
| import trimesh |
| import tempfile |
| import os |
| import warnings |
|
|
| |
| import asyncio |
|
|
| def load_laz(file_path): |
| """Load LAZ/LAS file and return all data.""" |
| las = laspy.read(file_path) |
| |
| |
| points = np.vstack([las.x, las.y, las.z]).T |
| centroid = points.mean(axis=0) |
| points = points - centroid |
| |
| |
| rgb = None |
| if hasattr(las, 'red') and len(las.red) > 0: |
| rgb = np.vstack([las.red, las.green, las.blue]).T |
| rgb = (rgb / 256).astype(np.uint8) |
| |
| |
| pred_instance = None |
| if 'PredInstance' in las.point_format.extra_dimension_names: |
| pred_instance = np.array(las['PredInstance']) |
| |
| |
| intensity = None |
| if hasattr(las, 'intensity'): |
| intensity = np.array(las.intensity) |
| |
| return { |
| 'points': points, |
| 'rgb': rgb, |
| 'pred_instance': pred_instance, |
| 'intensity': intensity, |
| 'total_count': len(points) |
| } |
|
|
| def get_instance_colors(pred_instance): |
| """Generate distinct colors for each instance.""" |
| |
| instance_colors = np.array([ |
| [128, 128, 128], |
| [255, 0, 0], |
| [0, 255, 0], |
| [0, 0, 255], |
| [255, 255, 0], |
| [255, 0, 255], |
| [0, 255, 255], |
| [255, 128, 0], |
| [128, 0, 255], |
| [0, 255, 128], |
| [255, 128, 128], |
| [128, 255, 128], |
| [128, 128, 255], |
| [255, 255, 128], |
| [255, 128, 255], |
| [128, 255, 255], |
| ], dtype=np.uint8) |
| |
| |
| colors = np.zeros((len(pred_instance), 3), dtype=np.uint8) |
| for i, inst in enumerate(pred_instance): |
| idx = int(inst) + 1 |
| idx = max(0, min(idx, len(instance_colors) - 1)) |
| colors[i] = instance_colors[idx] |
| |
| return colors |
|
|
| def get_elevation_colors(points, colormap_name): |
| """Apply elevation-based colormap.""" |
| import matplotlib.pyplot as plt |
| |
| z = points[:, 2] |
| z_norm = (z - z.min()) / (z.max() - z.min() + 1e-8) |
| |
| cmap = plt.get_cmap(colormap_name) |
| colors = (cmap(z_norm)[:, :3] * 255).astype(np.uint8) |
| |
| return colors |
|
|
| def get_intensity_colors(intensity): |
| """Convert intensity to grayscale colors.""" |
| i_norm = (intensity - intensity.min()) / (intensity.max() - intensity.min() + 1e-8) |
| gray = (i_norm * 255).astype(np.uint8) |
| return np.stack([gray, gray, gray], axis=1) |
|
|
| def load_example_file(example_name): |
| """Load example file and return the file path.""" |
| import os |
| base_dir = os.path.dirname(__file__) |
| example_path = os.path.join(base_dir, example_name) |
| if os.path.exists(example_path): |
| return example_path |
| return None |
|
|
| def visualize(file, color_mode, colormap, max_points): |
| """Main visualization function.""" |
| if file is None: |
| return None, "β οΈ Please upload a LAZ/LAS file" |
| |
| try: |
| |
| data = load_laz(file) |
| points = data['points'] |
| total = data['total_count'] |
| |
| |
| if len(points) > max_points: |
| indices = np.random.choice(len(points), int(max_points), replace=False) |
| points = points[indices] |
| rgb = data['rgb'][indices] if data['rgb'] is not None else None |
| pred_instance = data['pred_instance'][indices] if data['pred_instance'] is not None else None |
| intensity = data['intensity'][indices] if data['intensity'] is not None else None |
| else: |
| indices = None |
| rgb = data['rgb'] |
| pred_instance = data['pred_instance'] |
| intensity = data['intensity'] |
| |
| |
| if color_mode == "RGB (Original)": |
| if rgb is not None: |
| colors = rgb |
| else: |
| colors = get_elevation_colors(points, colormap) |
| elif color_mode == "Instance Segmentation": |
| if pred_instance is not None: |
| colors = get_instance_colors(pred_instance) |
| else: |
| return None, "β This file does not have PredInstance data" |
| elif color_mode == "Elevation": |
| colors = get_elevation_colors(points, colormap) |
| elif color_mode == "Intensity": |
| if intensity is not None: |
| colors = get_intensity_colors(intensity) |
| else: |
| colors = get_elevation_colors(points, colormap) |
| else: |
| colors = get_elevation_colors(points, colormap) |
| |
| |
| alpha = np.full((len(colors), 1), 255, dtype=np.uint8) |
| colors_rgba = np.hstack([colors, alpha]) |
| |
| |
| cloud = trimesh.PointCloud(points, colors=colors_rgba) |
| |
| tmp = tempfile.NamedTemporaryFile(suffix='.glb', delete=False) |
| tmp.close() |
| cloud.export(tmp.name, file_type='glb') |
| |
| |
| instance_info = "" |
| if pred_instance is not None and color_mode == "Instance Segmentation": |
| unique, counts = np.unique(pred_instance, return_counts=True) |
| instance_info = "\n\n**Instance Breakdown:**\n" |
| for u, c in sorted(zip(unique, counts), key=lambda x: -x[1])[:10]: |
| instance_info += f"- Instance {int(u)}: {c:,} pts\n" |
| |
| stats = f""" |
| ### π Point Cloud Statistics |
| | Property | Value | |
| |----------|-------| |
| | Total Points | {total:,} | |
| | Displayed | {len(points):,} | |
| | X Range | {points[:,0].min():.2f} to {points[:,0].max():.2f} | |
| | Y Range | {points[:,1].min():.2f} to {points[:,1].max():.2f} | |
| | Z Range | {points[:,2].min():.2f} to {points[:,2].max():.2f} | |
| | Color Mode | {color_mode} | |
| | Has RGB | {'β
' if data['rgb'] is not None else 'β'} | |
| | Has Segmentation | {'β
' if data['pred_instance'] is not None else 'β'} | |
| {instance_info} |
| """ |
| |
| return tmp.name, stats |
| |
| except Exception as e: |
| import traceback |
| return None, f"β Error: {str(e)}\n```\n{traceback.format_exc()}\n```" |
|
|
| |
| with gr.Blocks(title="GeoSpatial-LiDAR-3D Point Cloud Visualizer") as demo: |
| gr.Markdown("# π GeoSpatial-LiDAR-3D Point Cloud Visualizer") |
| gr.Markdown("Upload LAZ/LAS files with support for RGB colors and instance segmentation") |
| |
| with gr.Row(): |
| with gr.Column(scale=1): |
| file_input = gr.File( |
| label="π Upload LAS/LAZ File", |
| file_types=[".las", ".laz"] |
| ) |
| |
| gr.Markdown("### π Or use an example:") |
| example_dropdown = gr.Dropdown( |
| choices=["tree_raw.laz", "tree_segmentation.laz"], |
| label="π Examples", |
| value="tree_raw.laz" |
| ) |
| btn_load_example = gr.Button("π Load Example", size="sm") |
| |
| color_mode = gr.Radio( |
| choices=["RGB (Original)", "Instance Segmentation", "Elevation", "Intensity"], |
| value="RGB (Original)", |
| label="π¨ Color Mode" |
| ) |
| |
| colormap = gr.Dropdown( |
| choices=["viridis", "terrain", "plasma", "inferno", "Spectral", "coolwarm"], |
| value="viridis", |
| label="Elevation Colormap (for Elevation mode)", |
| visible=True |
| ) |
| |
| max_points = gr.Slider( |
| 50000, 2000000, |
| value=500000, |
| step=50000, |
| label="π Max Points to Display" |
| ) |
| |
| btn = gr.Button("π Visualize", variant="primary", size="lg") |
| |
| gr.Markdown(""" |
| --- |
| ### π¨ Color Modes |
| - **RGB**: Original colors from file |
| - **Instance Segmentation**: Color by PredInstance (if available) |
| - **Elevation**: Color by height (Z) |
| - **Intensity**: Grayscale by intensity |
| """) |
| |
| with gr.Column(scale=2): |
| model_output = gr.Model3D( |
| label="3D Visualization", |
| height=550, |
| clear_color=(0.1, 0.1, 0.15, 1.0) |
| ) |
| stats_output = gr.Markdown("*Upload a file and click Visualize*") |
| |
| btn.click( |
| visualize, |
| inputs=[file_input, color_mode, colormap, max_points], |
| outputs=[model_output, stats_output] |
| ) |
| |
| |
| def load_and_visualize_example(example_name): |
| import os |
| if example_name is None: |
| return None, "β οΈ Please select an example file" |
| base_dir = os.path.dirname(os.path.abspath(__file__)) |
| example_path = os.path.join(base_dir, "example", example_name) |
| if os.path.exists(example_path): |
| return visualize(example_path, "RGB (Original)", "viridis", 500000) |
| return None, f"β Example file '{example_name}' not found at {example_path}" |
| |
| btn_load_example.click( |
| load_and_visualize_example, |
| inputs=[example_dropdown], |
| outputs=[model_output, stats_output] |
| ) |
|
|
| if __name__ == "__main__": |
| |
| |
| demo.launch(ssr_mode=False, mcp_server=False) |
|
|