Chris Parmer — home

Synthetic LiDAR point cloud

Remote Sensing — building + ground + trees

Example from the compendium of canonical charts

Remote Sensing — Synthetic LiDAR point cloud (building + ground + trees)

Python Code

"""Remote Sensing — Synthetic LiDAR point cloud (building + ground + trees)."""
from pathlib import Path

# ── Palette + theme (matching the Plotly Studio gallery these charts ship in) ──
VIOLET, TEAL, GREEN, PINK, ORANGE = "#845EEE", "#52B3D0", "#55B685", "#DA5597", "#E9A23B"
PRIMARY, SECONDARY = VIOLET, TEAL
COLORWAY = [VIOLET, TEAL, GREEN, PINK, ORANGE]
BG, TEXT, GRID, MUTED = "#ffffff", "#1c2024", "#d9d9e0", "#60646c"
FONT = "Inter, -apple-system, BlinkMacSystemFont, sans-serif"
COLORSCALE = [[0, "rgba(132, 94, 238, 0.05)"], [1, "rgba(132, 94, 238, 0.9)"]]


def apply_theme(fig):
    """Light gallery theme: white background, Inter font, soft gridlines."""
    fig.update_layout(
        paper_bgcolor=BG, plot_bgcolor=BG, colorway=COLORWAY,
        font=dict(family=FONT, color=TEXT, size=12),
        legend=dict(font=dict(color=TEXT)),
        hoverlabel=dict(bgcolor="#f0f0f3", font=dict(color=TEXT, family=FONT), bordercolor=GRID),
    )
    fig.update_xaxes(gridcolor=GRID, linecolor=GRID, zerolinecolor=GRID)
    fig.update_yaxes(gridcolor=GRID, linecolor=GRID, zerolinecolor=GRID)


def fetch_csv(url, **kwargs):
    import io
    import pandas as pd
    import requests
    r = requests.get(url, timeout=60)
    r.raise_for_status()
    return pd.read_csv(io.StringIO(r.text), **kwargs)


def fetch_json(url):
    import requests
    r = requests.get(url, timeout=60)
    r.raise_for_status()
    return r.json()

import numpy as np
import plotly.graph_objects as go


def generate():
    RNG = np.random.default_rng(7)

    pts_x, pts_y, pts_z, pts_cls = [], [], [], []

    # ── Ground plane (class 2) ────────────────────────────────────────────────────
    n_ground = 5000
    gx = RNG.uniform(0, 100, n_ground)
    gy = RNG.uniform(0, 100, n_ground)
    gz = RNG.normal(0, 0.05, n_ground)
    pts_x.append(gx); pts_y.append(gy); pts_z.append(gz)
    pts_cls.append(np.full(n_ground, 2))

    # ── Building (class 6) — rectangular prism 40×40×15 centered at (50,50) ──────
    # Walls
    n_wall = 1500
    for x0, x1, y0, y1 in [(30, 70, 30, 30), (30, 70, 70, 70),
                            (30, 30, 30, 70), (70, 70, 30, 70)]:
        n = n_wall // 4
        wx = RNG.uniform(x0, x1, n) if x0 != x1 else np.full(n, x0)
        wy = RNG.uniform(y0, y1, n) if y0 != y1 else np.full(n, y0)
        wz = RNG.uniform(0, 15, n)
        pts_x.append(wx); pts_y.append(wy); pts_z.append(wz)
        pts_cls.append(np.full(n, 6))

    # Roof
    n_roof = 800
    rx = RNG.uniform(30, 70, n_roof)
    ry = RNG.uniform(30, 70, n_roof)
    rz = np.full(n_roof, 15.0) + RNG.normal(0, 0.05, n_roof)
    pts_x.append(rx); pts_y.append(ry); pts_z.append(rz)
    pts_cls.append(np.full(n_roof, 6))

    # ── Trees (class 5) — cones at random positions outside building ──────────────
    tree_positions = [(15, 15), (85, 20), (10, 80), (80, 75), (20, 55)]
    for tx, ty in tree_positions:
        n_tree = 300
        h_max = RNG.uniform(6, 12)
        tz = RNG.uniform(0, h_max, n_tree)
        radius = (h_max - tz) / h_max * 3.0 + RNG.normal(0, 0.2, n_tree)
        theta = RNG.uniform(0, 2 * np.pi, n_tree)
        tree_x = tx + radius * np.cos(theta)
        tree_y = ty + radius * np.sin(theta)
        pts_x.append(tree_x); pts_y.append(tree_y); pts_z.append(tz)
        pts_cls.append(np.full(n_tree, 5))

    X = np.concatenate(pts_x)
    Y = np.concatenate(pts_y)
    Z = np.concatenate(pts_z)

    print(f"Total points: {len(X)}")

    fig = go.Figure(go.Scatter3d(
        x=X,
        y=Y,
        z=Z,
        mode="markers",
        marker=dict(
            size=2,
            color=Z,
            colorscale="earth",
            colorbar=dict(title="Elevation (m)", thickness=12),
            opacity=0.8,
        ),
        hovertemplate="X: %{x:.1f}m<br>Y: %{y:.1f}m<br>Z: %{z:.1f}m<extra></extra>",
    ))

    fig.update_layout(
        scene=dict(
            xaxis_title="Easting (m)",
            yaxis_title="Northing (m)",
            zaxis_title="Elevation (m)",
            aspectmode="manual",
            aspectratio=dict(x=1.5, y=1.5, z=0.3),
            camera=dict(eye=dict(x=1.5, y=-1.8, z=1.2)),
        ),
    )

    apply_theme(fig)
    return fig


fig = generate()
fig.show()

Made with Plotly