Semana 28: Submuestreo de LiDAR y simulación de ruido cósmico
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

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

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

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
