recorte cargado: (1200, 1200) | válido: 0.958 | altura máx: 30.0 m
Parte 2 · TDA sobre imágenes, el agregador LSE(τ) y la práctica
CIMAT Mérida
8 de julio de 2026
\underbrace{\text{nube}\,(x,y,z)}_{\text{LiDAR}} \;\longrightarrow\; \underbrace{\text{DSM}}_{\text{dosel}} - \underbrace{\text{DTM}}_{\text{suelo}} = \underbrace{\text{CHM}}_{\text{altura}}
Celda a celda sobre la malla:
CHM LiDAR de Zambezi (recorte del curso): una altura por celda, 1 m/celda.
En el notebook (§0), tras el setup (que hace import tda_utils as T y define DATA), cargamos el recorte. load_chm usa rasterio y deja los nodata como NaN:
Salida:
recorte cargado: (1200, 1200) | válido: 0.958 | altura máx: 30.0 m
chm_full ya es una imagen (matriz de alturas); el ~4 % de celdas NaN (agua/huecos) son los píxeles blancos de la figura. Todo lo que sigue la trata como la función escalar f.
CHM y diagrama H₀ de una ventana de trabajo (400 m) de Zambezi —un zoom del sitio de la slide anterior. Verde = copas significativas; gris = ruido.
No toda prominencia es estructura. Las diminutas (\pi \approx 0) son el ruido vertical del sensor. Necesitamos un umbral \varepsilon_\text{min} defendible.
Idea (bootstrap de Fasy et al., 2014), en versión práctica:
Para comparar dos diagramas se emparejan sus puntos uno a uno (y los que sobran, contra la diagonal). La distancia bottleneck d_B es el menor \varepsilon para el que existe un emparejamiento donde cada punto se mueve \le \varepsilon — o sea, el movimiento más grande del mejor emparejamiento.
import gudhi as gd
def dgm(p): # diagrama H0 como pares (nacimiento, muerte)
f = np.isfinite(p.saddle)
return np.column_stack([-p.apex[f], -p.saddle[f]])
tile_ruido = tile + rng.normal(0, 0.30, tile.shape) # ruido del sensor, σ ≈ 0.30 m
d_B = gd.bottleneck_distance(dgm(pers), dgm(T.superlevel_persistence(tile_ruido)))Esto es una iteración del bootstrap de \varepsilon_\text{min}: repetirlo B veces da la banda c_n (cuantil 95 %), y por estabilidad \varepsilon_\text{min} = 2\,c_n.
Una sola perturbación mueve el diagrama d_B \approx 0.76 m. Repetido B veces, el cuantil 95 % de esas distancias es c_n \approx 0.92 m, y \varepsilon_\text{min} = 2\,c_n \approx 1.85 m.
Sobre el CHM público de Zambezi: un pico enorme de prominencias diminutas (ruido del sensor) y una cola larga de copas reales. El umbral \varepsilon_\text{min} corta entre ambos.
La persistencia no solo localiza las copas: las jerarquiza por prominencia. Y el corte es un número explícito y reproducible, con base estadística.
Todo lo anterior fue H₀ (copas = componentes conexas). La otra dimensión de la homología, H₁, cuenta agujeros — y en un CHM un agujero es un claro del bosque: un hueco de canopia baja encerrado por dosel alto (caída de un árbol, perturbación, entrada de luz).
Al bajar el “nivel del mar” sobre el CHM, un claro:
Toda la detección es una función: canopy_gaps corre H₁ y aplica el filtro Brokaw con tres umbrales ecológicos (los huecos sobre nodata/agua se descartan).
ring_min — dosel circundante mínimo (m): un hueco en dosel alto.floor_max — fondo máximo (m): que el hueco sea realmente bajo.depth_min — profundidad mínima (m): un claro real, no ruido.Salida:
generadores H₁: 22,503 | claros significativos: 15
Los 15 claros sobre el CHM (izquierda, color = profundidad) y en el diagrama H₁ (derecha; gris = ruido, magenta = significativos). Es el reverso del inventario de copas: no las cimas, sino los huecos —que regulan luz, regeneración y disturbio.
El TDA se corre por ventanas (tiles). El tamaño de ventana define qué estructura ves:
También hay que descartar el borde de cada tile: copas cortadas por el borde dan prominencias infladas (mueren en el “abismo” del borde, no en su silla real).
Del diagrama de persistencia de cada ventana extraemos descriptores que sí ven la estructura:
| Descriptor | Qué mide ecológicamente |
|---|---|
| nº de copas significativas | fragmentación / densidad de prominencias del dosel |
| prominencia media / p95 | relieve del dosel (liso vs. accidentado) |
| entropía de persistencia | diversidad estructural (¿pocas copas dominan o hay un espectro amplio?) |
| altura p95, cobertura | (referencia clásica, para contraste) |
Todo sale de una función; para el tile de trabajo:
Salida:
n_sig=2841, prom_media=3.11, prom_p95=5.57, pers_entropy=7.88, altura_p95=24.79, cobertura=1.00
1. Cada ventana se resume en su vector de descriptores (nº copas, prominencia, entropía…), que la vuelve un punto en un espacio de características.
2. k-means busca k grupos: asigna cada punto a su centro más cercano, mueve los centros al promedio de los suyos, y repite hasta estabilizar.
3. Cada grupo es una región estructural (un color). Ventanas del mismo color se parecen, aunque no estén pegadas en el mapa.
Los descriptores se calculan sobre la ventana, así que cambian con su tamaño → por eso la partición cambia con la escala (slide siguiente).
Cada punto es una ventana; los colores son los grupos que halla k-means.
regionalize encadena todo el bloque sobre las ventanas del recorte: (1) parte el CHM en teselas y calcula sus descriptores (chm_descriptors), (2) los estandariza, (3) agrupa las ventanas con k-means en k regiones y (4) devuelve el mapa de etiquetas (una región por ventana).
Salida:
ventanas clasificadas: 140 | ε_min = 1.87 m | regiones (k) = 4
Cada etiqueta de región es un color en el mapa: R["label_map"] guarda la región de cada ventana; R["feat"], sus descriptores.
La misma partición sobre Zambezi a 50 / 100 / 200 m. Cada color es una de las 4 regiones que agrupa k-means por sus descriptores TDA: mismo color = estructura de dosel parecida (las etiquetas son categóricas, no un orden; importa qué ventanas comparten color). Ventanas chicas resuelven copas y rodales; grandes, el paisaje.
La topología no elimina la escala ecológica: la hace explícita. Elegir la ventana es elegir la pregunta (copa, rodal o paisaje).
Agrupamos las mismas ventanas de Zambezi de dos maneras: solo con altura (p95, cobertura) y añadiendo los descriptores TDA. Las particiones difieren (ARI ≈ 0.25).
En un bosque heterogéneo como Zambezi la altura ya lleva mucha señal, así que coinciden en lo grueso pero difieren en el detalle. Altura y topología no compiten: se complementan —la topología ve organización del dosel que el promedio no distingue.
Hasta aquí asumimos que el CHM era confiable. Pero el CHM no es un dato bruto: es una decisión de agregación — y como el TDA mide forma, esa decisión cambia la topología que medimos.
Un CHM sale de la nube de puntos aplicando un agregador por celda (máximo, media, p95…). Lo habitual es el máximo duro (f_{\max}): la altura del retorno más alto. Pero hay una pregunta metodológica que casi nadie hace:
La elección del agregador de altura no es preprocesamiento neutral: condiciona la topología que vas a medir. La pregunta no es qué tan bien estima la altura, sino qué objeto produce para el TDA.
En doseles bajos y uniformes esto puede volverse un problema real: sobre un CHM de máximo duro la topología puede resultar frágil e irreproducible. Ese es el mecanismo que motiva usar un agregador suave como LSE.
Una envolvente superior suave por log-sum-exp (LSE), por celda:
f_\tau(c) = \tau \,\log\!\Big(\tfrac{1}{n_c}\textstyle\sum_{i\in c} e^{h_i/\tau}\Big)
El suavizado va dentro del estimador, no sobre un CHM ya hecho. Por celda partimos de la suma de pesos exponenciales S_\tau y el conteo n:
S_\tau(c)=\textstyle\sum_{i\in c} e^{h_i/\tau}, \qquad n(c)=\#\{\text{retornos en } c\}
Suavizamos ambos por separado con el mismo kernel gaussiano (soporte σ = 1 m) y tomamos el cociente:
f_\tau^{\text{(prod)}}(c) = \tau \log\!\Big[\frac{G_\sigma\{S_\tau\}(c)}{G_\sigma\{n\}(c)}\Big]
Es una convolución normalizada por conteo: el análogo espacial del 1/n del LSE por celda.
Se suaviza antes del log (en el dominio exponencial): así una celda sin retorno puede “pedir prestada” masa exponencial y su conteo a las vecinas.
A 0.5 m la densidad de retornos varía: muchas celdas tienen 0 o 1 retorno, y ahí el LSE por celda falla:
El suavizado deja que cada celda tome retornos de su vecindad (~1 m): rellena los huecos y le da suficientes retornos para que la reducción de varianza del LSE de verdad actúe.
No basta con “¿es robusto?”. Un buen agregador para TDA debería cumplir tres ejes independientes:
1 · Robustez
Perturba el CHM con ruido del sensor. ¿Cuánto se mueve la superficie (celda a celda)? Menos movimiento = más robusto.
2 · Fidelidad de altura
¿Concuerda con una medida externa independiente (p. ej. GEDI L2A satelital)? Que no sea robusto a costa de mentir sobre la altura.
3 · Reproducibilidad topológica
El eje que casi nadie mide: ¿dos mitades de la nube dan el mismo diagrama H₀? Es la prueba que le importa al TDA.
Un agregador puede ganar en robustez y fallar en reproducibilidad topológica. Ese es justo el riesgo que este tercer eje detecta —y el que motiva calibrar el agregador.
Si el método fuera reproducible, partir la nube de puntos en dos mitades disjuntas y correr todo por separado debería dar la misma descripción topológica del dosel.
Distancia pequeña y estable ⇒ la topología que reportas no es un accidente de qué mitad de los fotones te tocó. Es reproducibilidad, no solo robustez al ruido.
¿Por qué dos ejes distintos?
Robustez = estabilidad de la superficie (métrica de metros).
Reproducibilidad = estabilidad del diagrama (métrica de topología).
Un promedio duro puede ser inestable en la superficie pero, peor aún, dar diagramas que no se parecen entre mitades: ruido topológico.
No es un test con receta universal: lo proponemos como un eje explícito para calibrar el agregador según la reproducibilidad de la topología que produce.
La construcción (split-half). El pipeline completo, por tesela, es
T:\ \text{puntos}\to \text{CHM}\to \operatorname{Dgm}_0 \to \{\pi_i\}.
Partimos los primeros retornos en dos mitades disjuntas X = X_1 \sqcup X_2 y comparamos T(X_1) con T(X_2). Disjuntas (no bootstrap con reemplazo): así los dos conjuntos no comparten puntos y la pregunta de muestreo queda limpia.
¿Por qué importa? La robustez y GEDI no preguntan si la topología que reportas sobrevive a remuestrear los datos crudos. Este eje sí — y es el que delata una topología frágil (la que produce un mal agregador).
Lo ideal sería repetir el vuelo/levantamiento varias veces y ver si sale la misma topología —pero es inviable (caro, y el dosel cambia entre vuelos). El split-half es el proxy factible: dos mitades de un solo vuelo.
¿Cómo se mide que “dos diagramas se parecen”? Emparejando sus puntos (y con la diagonal para los que sobran):
El teorema de estabilidad (Cohen–Steiner) garantiza d_B(\text{Dgm}\,f,\text{Dgm}\,g)\le \|f-g\|_\infty: si el CHM cambia poco, el diagrama cambia poco. Pero eso vale para una superficie perturbada; la reproducibilidad split-half pregunta algo más fuerte —que el diagrama sobreviva a remuestrear los datos crudos.
Mismo emparejamiento entre dos diagramas: el bottleneck solo cuenta el peor movimiento (rojo); el Wasserstein-1 suma todos (grosor ∝ longitud). Por eso W_1 siente muchos cambios pequeños que el bottleneck ignora — y por eso es el que usamos para la reproducibilidad.
Honestidad sobre el alcance del diagnóstico:
El raster público no tiene retornos, así que no podemos calibrar τ sobre él. Construimos una nube de puntos sintética sobre un dosel cuyo número real de copas conocemos:
# 1) dosel verdadero = máximo de gaussianas → copas CONOCIDAS (ground truth)
f_true = canopy(centers, amps, widths, base)
# 2) nube de retornos por celda: tope − penetración + ruido + pico espurio ocasional
def simulate_returns(f, n=12, sigma=0.15, pen=0.8, p_out=0.03, out=5.0):
base = f[..., None]
return (base - rng.exponential(pen, (N, N, n)) # penetración
+ rng.normal(0, sigma, (N, N, n)) # ruido del sensor
+ (rng.random((N, N, n)) < p_out) * rng.uniform(1, out, (N, N, n))) # espurios
Z = simulate_returns(f_true)Con ground truth podemos validar si el agregador recupera las copas que de verdad hay, algo que ningún dato de campo permite (Parte II del notebook).
fmax = chm_max(Z) # máximo duro
flse = chm_lse(Z, tau=4.0) # LSE, f_τ (log-sum-exp)
fig, axs = plt.subplots(1, 3, figsize=(11, 3.5))
for ax, img, ttl in [(axs[0], f_true, f"verdad — {G_true} copas"),
(axs[1], fmax, f"máximo duro — {n_significant(fmax)} copas"),
(axs[2], flse, f"LSE τ=4 — {n_significant(flse)} copas")]:
im = ax.imshow(img, cmap="viridis", vmin=f_true.min(), vmax=f_true.max())
ax.set_title(ttl); ax.axis("off")
fig.colorbar(im, ax=axs, shrink=0.7, label="altura (m)")
plt.show()El dosel sintético tiene 27 copas reales. El máximo duro reporta cientos (ruido de un solo retorno espurio); el LSE las recupera. El agregador decide si el TDA ve estructura o ruido.
“Copas” = generadores H₀ con prominencia > 1.5 m (corte fijo, por ser sintético; en Zambezi el umbral es el \varepsilon_\text{min} del bootstrap).
Barremos τ y medimos, contra la verdad, el número de copas y el sesgo de altura. Al subir τ el conteo converge a la verdad (el codo → τ* ≈ 4); un τ demasiado grande sesga la altura (el LSE tiende a la media).
τ* no es un óptimo universal: es un default calibrado, el equilibrio entre matar el ruido y no sobre-suavizar. Robustez y reproducibilidad lo empujan arriba; el sesgo de altura lo frena.
¿Cuándo importa τ? Depende de la profundidad de las sillas entre copas:
Los descriptores topológicos caracterizan la estructura del dosel. Un paso aplicado natural es estimar la biomasa aérea y el carbono almacenado, relevante para el monitoreo de blue carbon en manglares.
La altura del dosel es un proxy de biomasa: una alometría calibrada en campo convierte altura en carbono. Usamos la misma que el análisis de carbono del proyecto (Stringer 2015 para Zambezi), y somos honestos sobre de dónde viene la incertidumbre.
Manglar denso: reservorio de blue carbon.
Para Zambezi usamos la alometría calibrada en campo de Stringer et al. 2015 —la misma del análisis de carbono del proyecto—: una relación stand-level directa de altura a carbono, sin densidad de tallos.
C_\text{AGB} = 59.6\,H^{0.562}\ \ [\text{Mg C/ha}] \;\xrightarrow{\times\,44/12}\; \text{CO}_2
Cada sitio necesita su propia calibración alométrica; el pipeline TDA es idéntico. Corre en vivo en el notebook.
Aplicando la alometría de Stringer al CHM público, celda a celda:
\approx 1060\ \text{tCO}_2/\text{ha}\quad(\approx 290\ \text{Mg C/ha})
¿Y propagar el ruido del sensor con el bootstrap de \varepsilon_\text{min}? Al promediar sobre millones de celdas se cancela: el IC del sensor es \approx 0.
La superficie es robusta; lo que no conocemos con precisión es la alometría.
Precisión ≠ exactitud. El CHM no es el cuello de botella: la incertidumbre la domina la alometría. La de Stringer está calibrada en campo para Zambezi; otro sitio necesita su propia calibración. Es una estimación con base de campo, no un stock certificado con parcelas nuevas en este recorte.
Como el máximo duro siempre es \ge el log-sum-exp (\text{máx} \ge \text{LSE}), el máximo tiende a sobreestimar la altura y, con ella, el carbono estimado tile a tile.
Para desarrollar. Medir cómo la elección de agregador se propaga a la topología (¿se detectan las mismas copas?), a su reproducibilidad y al carbono estimado —comparando máx vs LSE(\tau) sobre un mismo levantamiento— queda como línea de trabajo. El mensaje del curso se sostiene: el input no es preprocesamiento neutral.
Un solo notebook autocontenido (notebook/tda_forestal.qmd; en Google Colab, un clic):
gudhi → diagrama H₀ y distribución de prominencias.Tip
Todo con datos públicos y scripts/tda_utils.py. La misma tubería se traslada a otro sitio cambiando solo la ruta del archivo.
Cada punto es un generador H₀ con \pi > \varepsilon_\text{min}, coloreado y dimensionado por prominencia. El TDA localiza las copas robustas sobre el CHM.
LSE calibrado).Ese contraste —mismo método, mecanismos distintos según el bosque— es lo que vuelve al TDA informativo: no da un número, da una descripción estructural comparable entre sitios.
Para no sobre-vender —qué no hace esto:
Nada de esto invalida el método; lo acota. El TDA da descriptores estructurales con supuestos explícitos —esa transparencia es parte del valor.
El eslabón que Zambezi no permite (raster sin retornos) sí se puede hacer donde hay nube de puntos:
1 · En el laboratorio sintético (público, reproducible). Construye el LSE (f_\tau) desde los retornos y corre H₀ + \varepsilon_\text{min} + H₁ sobre él; compáralo con el máximo duro.
2 · Reto (tu propia nube). Si tienes un LAS, reconstruye el CHM con chm_lse y corre el pipeline completo, comparándolo con el máximo duro sobre tu sitio.
Es la mejor forma de ver que el agregador no es neutral: mismo TDA, distinto CHM, distinta topología.
notebook/tda_forestal.qmd — de la descarga al carbono, más el laboratorio sintético de τ. Corre en Google Colab con un clic.scripts/tda_utils.py (persistencia, umbral, descriptores, regiones, claros).data/zambezi/ (NASA CMS #1357).Lecturas mínimas
gudhi.inria.fr.Tip
¿Preguntas? El mejor siguiente paso es abrir el notebook y cambiar parámetros: \sigma, tamaño de ventana, número de regiones, y τ en el laboratorio. La intuición se construye jugando.
LiDAR + TDA para estudios ecológicos — Parte 2