3 minute read

Submuestreo de haces y rayos

Esta semana se ha trabajado en la reducción controlada de la resolución del LiDAR, tanto vertical como horizontal, para simular sensores con distintas configuraciones.
El objetivo es poder obtener datasets sintéticos comparables a sensores reales, manteniendo el control sobre la estructura de la nube de puntos.

Imágen por defecto

Imágen por defecto

Submuestreo por haces (vertical)

Cada LiDAR posee un número de canales verticales o haces. Para simular sensores de menor resolución, se ha implementado un submuestreo que elimina haces completos, conservando solo 1 de cada N.

Primero se calcula el ángulo de elevación de cada punto y se le asigna un índice de haz (ring_id) según su posición vertical:

θ = arctan2(z, √(x² + y²))
ring = round( ((θ - θ_min) / (θ_max - θ_min)) * (C - 1) )

Luego se conserva únicamente los puntos cuyo ring_id % step == 0, eliminando los haces intermedios.

Parámetro Descripción
step Factor de submuestreo vertical (por ejemplo, step=4 → 1 de cada 4 haces)
channels Número original de canales
lower_fov, upper_fov Límites verticales del FOV del sensor

Código para calcular el id del haz

def calculate_ring_id(points, channels, lower_fov, upper_fov):
    # elevación en grados
    xy = np.hypot(points[:, 0], points[:, 1]) # sqrt(x^2 + y^2), distancia al eje Z
    elevation = np.degrees(np.arctan2(points[:, 2], xy))  # Elevación en grados

    vertical_fov = (upper_fov - lower_fov)
    # Evitar divisiones raras si el FOV es 0
    vertical_fov = vertical_fov if vertical_fov != 0 else 1e-6
    t = (elevation - lower_fov) / vertical_fov

    # Cuantizar a [0, channels-1]
    ring = np.round(t * (channels - 1)).astype(np.int32)
    ring = np.clip(ring, 0, channels - 1)
    return ring

Código de submuestreo por haces

def subsample_by_ring_id(points, semantic_tags, ring_id, step):
    mask = (ring_id % step) == 0
    return points[mask], semantic_tags[mask], mask

Resultado

Submuestreo por haces (step=4)

Este método elimina canales completos manteniendo la geometría de los haces, simulando LiDARs de 32 o 16 canales con alta fidelidad.


Submuestreo por rayos (horizontal)

Además de los canales verticales, cada haz emite miles de rayos.
Para simular una menor resolución angular, se desarrolló un submuestreo que selecciona solo 1 de cada M rayos por haz.

Para cada anillo (ring_id), se calculan los ángulos azimutales:

φ = arctan2(y, x)

Los puntos de cada haz se ordenan por su ángulo (\phi), y se conserva 1 de cada step_az puntos.
De este modo se mantiene la estructura circular del escaneo, reduciendo la densidad horizontal sin producir grandes cortes.

Parámetro Descripción
step_ray Factor de submuestreo horizontal (por ejemplo, step_az=3 → 1 de cada 3 rayos)

Código de submuestreo por rayos

def subsample_by_rays(points, semantic_tags, ring_id, step_ray=2):
    N = len(points)

    # Azimutal en grados
    az = np.degrees(np.arctan2(points[:, 1], points[:, 0]))
    az = (az + 360.0) % 360.0   # [0, 360)

    mask = np.zeros(N, dtype=bool)
    unique_rings = np.unique(ring_id)

    for r in unique_rings:
        idx = np.where(ring_id == r)[0]
        order = np.argsort(az[idx])          # Ordena los puntos de ese haz por azimutal
        keep  = idx[order][::step_ray]       # 1 de cada step_ray
        mask[keep] = True

    return points[mask], semantic_tags[mask], mask

Resultado

Submuestreo por rayos

El método produce una reducción de densidad realista y sin artefactos, similar a sensores con menor frecuencia angular.


Simulación de ruido cósmico

También se ha añadido un modelo simple de ruido cósmico, que introduce puntos falsos aleatorios dentro del campo de visión del LiDAR. Cada punto falso se genera con coordenadas esféricas aleatorias:

r ~ U(0.1, R_max)
φ ~ U(−HFOV/2, HFOV/2)
θ ~ U(θ_min, θ_max)

y se transforma a coordenadas cartesianas locales:

x = r·cosθ·cosφ
y = r·cosθ·sinφ
z = r·sinθ

Por último, se transforman a coordenadas globales según la posición del sensor y por ahora se etiquetan con un valor especial -1 para poder presenciarlos claramente durante las pruebas.

Parámetro Descripción
rate Proporción de puntos falsos añadidos (por ejemplo, 0.001 = 0.1%)
max_range, hfov, upper_fov, lower_fov Parámetros del sensor

Código de ruido cósmico

def add_cosmic_noise_points(points, semantic_tags, max_range, 
                            hfov, upper_fov, lower_fov,
                            rate=0.001):
    N = points.shape[0]
    n_fake = int(N * rate)
    if n_fake == 0:
        return points, semantic_tags

    # Generar coordenadas XYZ aleatorias en el rango del LiDAR
    az = np.radians(np.random.uniform(-hfov/2, hfov/2, n_fake))
    el = np.radians(np.random.uniform(lower_fov, upper_fov, n_fake))
    r  = np.random.uniform(0.1, max_range, n_fake)

    x = -r * np.cos(el) * np.cos(az)
    y = r * np.cos(el) * np.sin(az)
    z = r * np.sin(el)

    fake_points_world = np.stack([x, y, z], axis=1)

    # Etiquetas de ruido cósmico (-1)
    fake_tags = np.full(n_fake, -1, dtype=np.int32)

    points_new = np.vstack([points, fake_points_world])
    semantic_tags_new = np.concatenate([semantic_tags, fake_tags])

    return points_new, semantic_tags_new

Resultado

Ruido cósmico LiDAR