Procesamiento del Mapa
El script extract_walls.py recibe los archivos current_map.yaml y current_map.pgm producidos por SLAM Toolbox durante la sesión de exploración del robot. A partir de esos archivos aplica una cadena de procesamiento de imagen para producir un JSON con los segmentos de pared que delimitan el espacio navegable y, opcionalmente, las personas detectadas en planta. Ese JSON es el que Unity consume para reconstruir la geometría del entorno en realidad mixta.
El mapa de ocupación en PGM
SLAM Toolbox escribe el mapa como una imagen en escala de grises en formato PGM. Cada píxel representa una celda del grid de ocupación:
| Rango de valor | Significado |
|---|---|
| >= 252 (blanco) | Espacio libre: el robot puede navegar aquí |
| ~128 (gris medio) | Zona desconocida: el LIDAR no ha llegado a cubrir esa área |
| ~0 (negro) | Obstáculo: pared, mueble u objeto sólido |
El archivo YAML que acompaña al PGM proporciona los metadatos necesarios para convertir coordenadas de imagen a coordenadas del mundo:
image: current_map.pgm
resolution: 0.05 # metros por píxel
origin: [-3.5, -2.1, 0.0] # [x, y, theta] del píxel (0,0) en el frame del mapa ROS
El campo resolution define cuántos metros representa cada píxel. El campo origin fija la posición en metros del píxel en la esquina superior izquierda (fila 0, columna 0) dentro del frame de coordenadas del mapa de ROS.
Pipeline de procesamiento
El procesamiento sigue estos pasos en orden:
-
Carga de archivos. El YAML se lee con PyYAML. La imagen PGM se carga con PIL en modo escala de grises y se convierte a un array NumPy de tipo
uint8. -
Umbralización. Se construye una imagen binaria donde los píxeles con valor
>= FREE_MINse marcan como espacio libre:FREE_MIN = 252Todo lo que quede por debajo de ese umbral se trata como obstáculo o zona desconocida.
-
Limpieza morfológica. Se aplican dos operaciones consecutivas sobre la imagen binaria:
MORPH_OPEN(kernel=3) -- elimina ruido puntual (píxeles aislados) MORPH_CLOSE(kernel=8) -- cierra pequeñas protuberancias en el contornoLa apertura con kernel 3 elimina grupos pequeños de píxeles que aparecen en el espacio libre por reflexiones del LIDAR. El cierre con kernel 8 suaviza las indentaciones menores del contorno que no corresponden a geometría real, sin destruir las indentaciones más grandes que sí pueden indicar la presencia de personas.
-
Componente conexo principal. El espacio libre puede quedar fragmentado en varias regiones no conectadas (pasillos separados, áreas fuera del campo de exploración). Se usa
cv2.connectedComponentsWithStatsy se retiene únicamente el componente de mayor área, siempre que supere el umbral mínimo:MIN_FREE_AREA_PERCENT = 0.01 # 1% del área total del mapa en píxeles -
Contorno interior. Sobre el componente principal se extrae el contorno con
cv2.findContoursusando el modoRETR_CCOMPy la aproximaciónCHAIN_APPROX_NONE. De todos los contornos devueltos se toma el primero (contours[0]), que corresponde al borde interno del espacio libre, es decir, la línea que separa el área navegable de las paredes. -
Aproximación poligonal. El contorno crudo tiene cientos o miles de puntos. Se simplifica con el algoritmo Ramer-Douglas-Peucker:
APPROX_EPS_PX = 12.0 # tolerancia en píxelesCon este valor, solo se conservan los vértices que implican un desvío geométrico mayor a 12 píxeles respecto a la línea que une sus vecinos. El resultado es un polígono con un número manejable de vértices que preserva los cambios de dirección significativos del contorno.
-
Segmentación. Cada par de vértices consecutivos del polígono simplificado se convierte en un segmento si su longitud supera el umbral mínimo:
MIN_SEG_LEN_PX = 5.0 # píxelesLos segmentos resultantes aún están en coordenadas de píxel.
Fusión de segmentos colineales
La aproximación poligonal puede producir segmentos cortos y casi alineados que en realidad pertenecen a la misma pared. Para consolidarlos se aplica un algoritmo de fusión iterativo.
Criterios de fusión. Dos segmentos se fusionan si cumplen simultáneamente las tres condiciones siguientes:
| Criterio | Parámetro | Valor |
|---|---|---|
| Diferencia de ángulo entre segmentos | COLINEAR_ANGLE_TOL_DEG | 25.0 grados |
| Distancia perpendicular de un extremo a la recta del otro | COLINEAR_DIST_TOL_PX | 15.0 px |
| Gap (distancia mínima entre extremos) | COLINEAR_GAP_TOL_PX | 12.0 px |
Cuando un grupo de segmentos supera los tres criterios, todos sus puntos extremos se proyectan sobre el eje de dirección del grupo y el segmento fusionado va del punto con proyección mínima al punto con proyección máxima. Esto garantiza que el segmento resultante cubre el tramo completo sin acortar ni alargar artificialmente.
Por qué se necesitan múltiples iteraciones. En una sola pasada, cuando el segmento A y el segmento C están separados por el segmento B, puede ocurrir que A y B se fusionen en la primera pasada y que el nuevo segmento A+B quede ahora alineado con C, siendo posible una segunda fusión que no era detectable al inicio. El script ejecuta hasta 5 iteraciones y se detiene antes si el número de segmentos no cambia entre dos pasadas consecutivas:
max_iterations = 5
Iteracion 1: N segmentos
Iteracion 2: N' segmentos (N' < N si hubo fusiones)
...
Se detiene cuando new_count == prev_count
El resultado es un conjunto reducido de segmentos en coordenadas de píxel listos para la conversión a metros.