Workshops
07 · Obstáculos fuera del mapa
Compará el /scan contra el mapa conocido y reportá la coordenada en map de lo que no debería estar ahí.
Al terminar, tu robot detecta objetos que el mapa no tiene registrados y publica la coordenada (x, y) en map de cada uno, visible como marcador fijo en RViz.
En esta guía
08 seccionesLa práctica
Comparar.
Agrupar.
Ubicar.
El mapa de la competencia es una foto fija; el terreno real no. Este workshop encuentra lo que cambió y le pone una coordenada.
- Compara
/scancontra/likelihood_map(el campo de semana 06)- Agrupa
- Rachas contiguas de puntos "no mapeados" en objetos discretos
- Publica
PoseArray+MarkerArraycon la coordenada enmapde cada uno- Reusa
- El truco de "scan filtrado con
inf" de/scan_rojo, semana 04
Antes de empezar
-
Semana 06 funcionando
Necesitás
localizadorycampo_verosimilitudde la semana 06 corriendo y corrigiendomap → odom— este workshop consume/likelihood_mapy la tf amapque ya publican. -
Un mundo con víctimas
Esta semana usás un mundo
_victimas(mismo laberinto, con cubos de color que no están en el mapa que le pasás amap_server) en vez del mundo "limpio" que veniás usando. -
Chequeo
Con el mapa "limpio" corrigiendo bien la pose (comprobación de semana 06), estás listo para crear el paquete de esta semana.
Concepto mínimo: comparar contra un campo que ya está calculado
La forma “directa” de comparar el /scan contra el mapa sería raycasting: para cada rayo, simular qué debería medir un lidar ideal parado en la pose actual, marchando celda por celda sobre la grilla. Es la misma cuenta que semana 06 descartó por lenta en Python puro — ahí, en vez de simular el lidar completo por cada partícula, se precalculó una sola vez un campo de verosimilitud (/likelihood_map) y después solo se lo indexa.
Ese campo ya es, literalmente, “qué tan probable es que el mapa diga que hay algo en esta celda”. Compararlo acá sale gratis: transformás cada punto del /scan a map (mismo lookup de tf2 que ya usaste en semana 04, con la tf completa que arma semana 06), lo convertís a un índice [fila, columna] de /likelihood_map, y leés la probabilidad — si es baja, el mapa no tenía nada ahí.
Un obstáculo real casi nunca genera un solo punto “no mapeado” suelto: genera una racha de varios rayos seguidos pegando en la misma cara, igual que ya viste con /scan_rojo. Agrupar esas rachas y promediar sus puntos en map alcanza para tener la coordenada del objeto, sin necesitar un clustering espacial genérico.
Un punto puede transformarse a una posición que cae afuera del área que cubre
/likelihood_map(el robot mirando hacia el borde del mapa conocido). Ahí no hay ninguna celda que consultar, y tratar “no hay dato” como “probabilidad baja” reporta objetos fantasma pegados al borde — sin importar cuánto ajustes el umbral. Hay que descartar ese caso aparte.
Implementación
Creá tu paquete
Creá tu paquete, llamado por ejemplo obstaculos_no_mapeados, y modificá los archivos correspondientes — mismo procedimiento que en semana 06. Copiá detector_obstaculos.py al paquete. Por último, hacé colcon build --symlink-install, otra vez, porque vas a editar los TODOs muchas veces.
Qué hay que completar
Toda la plomería está resuelta: parámetros, suscripciones (/scan en qos_profile_sensor_data, /likelihood_map en Transient Local), el lookup de la transformada a map, y la publicación del LaserScan filtrado, el PoseArray y el MarkerArray. Quedan 2 funciones con TODO:
agrupar_en_rachas()— encontrar las rachas contiguas de puntos “no mapeados” en el array del scan. Ejercicio de arrays con numpy, sin librerías nuevas; el docstring guía paso a paso.recibir_scan()— el corazón del workshop: transformar el scan amap, indexar contra/likelihood_map, y decidir qué puntos son “no mapeados” (incluyendo el descarte del borde de arriba). Reusá acá lo que ya escribiste en semana 04 (polar → cartesiano) y lo que ya viste en semana 06 (pesar_particulas, para el índice de fila/columna).
Completalas en ese orden: agrupar_en_rachas() se puede probar aislada, sin ROS, antes de meterte con recibir_scan().
| Parámetro | Default | Qué es |
|---|---|---|
map_frame |
map |
Frame contra el que se reportan las coordenadas. |
umbral_probabilidad |
25.0 | Probabilidad (0-100) por debajo de la cual un punto se considera “no mapeado”. Sin calibrar a fondo — ver Desafío extra. |
racha_minima |
3 | Cuántos puntos contiguos hacen falta para que una racha cuente como objeto. |
distancia_minima_valida |
0.25 | Rango mínimo (m) válido — descarta el rebote del lidar contra la propia estructura del robot. |
Ejecución
Sumá detector_obstaculos a tu propio launch de semana 06 (un Node más en la LaunchDescription, mismo patrón que ya usaste para agregar localizador y campo_verosimilitud). Dos cosas puntuales que cambian esta semana:
-
Usá un mundo
_victimasen vez del mundo “limpio” que veniás usando — son el mismo laberinto, pero con un par de cubos de color puestos en el mundo que no están en el mapa que le pasás amap_server(que sigue siendo el mismo.yamlde siempre, sin cambios). Esos cubos son justo los objetos “no mapeados” que este workshop tiene que encontrar; si corrés el mundo sin víctimas no vas a tener nada que detectar (lo cual también sirve para confirmar que no te tira falsos positivos contra las paredes reales).Para este workshop hace falta el par completo (mundo
_victimasy su.yaml). Para ver qué pares existen:comm -12 \ <(ls "$(ros2 pkg prefix yahboom_rosmaster_gazebo)/share/yahboom_rosmaster_gazebo/worlds/" | grep victimas | sed 's/_victimas\.world$/.yaml/' | sort) \ <(ls "$(ros2 pkg prefix yahboom_rosmaster_gazebo)/share/yahboom_rosmaster_gazebo/maps/" | sort)(
comm -12compara dos listas ordenadas y se queda solo con lo que aparece en las dos — acá, los mundos_victimasa los que les sacás el sufijo y les ponés.yaml, contra los mapas que existen de verdad.) De ejemplo vamos a usarlaberinto_simple.yaml. -
Agregale a tu config de RViz los displays nuevos:
LaserScan(scan_no_mapeado, en Best Effort — mismo QoS que elLaserScande/scanque ya tenías),PoseArray(objetos_no_mapeados) yMarkerArray(objetos_no_mapeados_markers).
Si
detector_obstaculosarranca antes de quemap_serveresté activo o antes de quelocalizadorhaya publicado la primeramap → odom, vas a ver el avisoTodavía no hay tf laser_link->map— es esperable, no un error: en cuanto exista esa tf, el nodo arranca a detectar solo, sin que haga falta reiniciarlo.
Comprobación
Manejá el robot cerca de una víctima y mirá en RViz:
- Aparece un marcador (esfera roja) quieto sobre la víctima, en su posición real dentro del mapa — sin importar desde dónde la mire el robot. Dale una vuelta alrededor y confirmá que el marcador no se mueve.
scan_no_mapeadosolo tiene puntos cerca de la víctima — si ves puntos pegados a las paredes reales, revisá el descarte del borde del Concepto mínimo (o bajáumbral_probabilidad).- Alejate y volvé a acercarte: el marcador se mantiene en la misma coordenada, aunque la distancia y el ángulo relativos al robot hayan cambiado por completo — esa es la comprobación central, estás reportando una posición del mundo, no una medición relativa.
Explicación
Con esto, cualquier nodo puede preguntar “¿hay algo que el mapa no tenga registrado, y dónde?” sin saber cómo se calculó esa respuesta — mismo patrón de desacople que map → odom en semana 06. Es, a mano, una versión simplificada de lo que hace la capa de obstáculos del local costmap de Nav2: un costmap combina el mapa estático con una ventana rodante de datos vivos del sensor, marcando como ocupado lo que el sensor ve pero el mapa no. Cuando llegue el workshop de Nav2, esa capa reemplaza a este nodo — pero el concepto de fondo es el mismo.
Si subís mucho
umbral_probabilidad, ¿qué tipo de error crece: falsos positivos o falsos negativos? ¿Y si lo bajás mucho?
Desafío extra
- Calibrar
umbral_probabilidadyracha_minimaen serio: los defaults funcionan enlaberinto_simple_victimas, pero no están calibrados contra otros mapas ni movimiento sostenido. Manejá un rato largo, contá a mano falsos positivos y negativos, y ajustá los dos parámetros — mismo criterio quesigma_sensoren semana 06. - Reportar la posición de una víctima roja: combiná este nodo con
/scan_rojode semana 04 — comparando/scan_rojo(ya filtrado a “rayos que además pegan en algo rojo”) contra/likelihood_mapen vez del/scancrudo. El resultado son las coordenadas de los objetos que son, a la vez, no mapeados y rojos: la víctima. No hace falta escribir nada nuevo, es cambiar qué tópico suscribís. - Clustering espacial en vez de por racha: agrupar por distancia real entre puntos en
map(por ejemplo conscipy.cluster.hierarchy.fclusterdata) en vez de por racha contigua del scan, para no pegar dos objetos separados vistos desde lejos en un mismo ángulo.