
Chisme del Lince AstroPi10: NDVI, la salud de las plantas desde el espacio
Víctor Vílchez, el Lince de la Noche, sube a la loma de Sierra Bermeja con los prismáticos. Abajo, los pinos que se quemaron en el incendio de 2021; más arriba, manchas verdes que vuelven a crecer. NDVI Raspberry Pi.
—Gata, quiero saber qué zonas se están recuperando. Me recorro el monte árbol por árbol y apunto cuáles están verdes.
—¿Todos? —pregunta la Gata Montesa—. Tardarías años. Y «verde» no es una medida.
—¿Y qué hago? ¿Subo a la ISS?
—Casi. Los satélites miden la salud de las plantas con una luz que tú no ves: el infrarrojo cercano. Una hoja sana lo refleja muchísimo. Con eso se calcula el NDVI.
—Pues no tengo satélite.
—Tienes una cámara NoIR en la Raspberry Pi. ¿Te acuerdas de por qué tus plantas salían rosas?
El Lince se queda mirando el monte. Aquello no era un fallo de la cámara. Era un dato.
—Y recuerda —añade la Gata—: Carl Sagan decía que miráramos ese puntito azul, porque es nuestro hogar. El espacio sirve, sobre todo, para cuidar los bosques de aquí.
Introducción
En el AstroPi04 conectamos la cámara NoIR y vimos que las plantas salían de color rosa. Hoy convertimos ese «fallo» en una medida: vamos a calcular el NDVI con Raspberry Pi, el índice que usan los satélites para saber si la vegetación está sana.
Seguimos la guía oficial Capture plant health with NDVI and Raspberry Pi y probamos el programa con tres fotos: la de un parque (con filtro azul), la de la terraza del Lince (sin filtro) y una foto real de la Astro Pi en la ISS.
Por qué este chisme
El NDVI es uno de los experimentos más populares de Mission Space Lab: una de las Astro Pi de la ISS, Izzy, lleva una cámara NoIR que mira a la Tierra por una ventana.
Además, el NDVI con Raspberry Pi enseña algo muy importante en ciencia: una imagen bonita no es una medida. Veremos que el mismo programa da un resultado fiable con el material adecuado y uno engañoso sin él.
Cómo funciona el NDVI
Las hojas sanas usan la luz roja para la fotosíntesis (la clorofila la absorbe) y reflejan muchísimo infrarrojo cercano. Una hoja seca, el suelo o el asfalto reflejan mucho menos infrarrojo.
El NDVI (Normalized Difference Vegetation Index) compara esas dos luces en cada píxel:
NDVI = (infrarrojo − rojo) ÷ (infrarrojo + rojo)
Sale un número entre −1 y +1. Cerca de +1, vegetación sana y densa; cerca de 0, suelo o desierto; negativo, agua, nubes o cosas sin vida.
Una cámara normal tiene un filtro que bloquea el infrarrojo. La NoIR no lo tiene, y la guía le pone delante un filtro azul: así un canal de color recoge sobre todo infrarrojo y otro, luz visible. El programa compara esos dos canales.
Qué necesitamos
- Raspberry Pi 4 con la cámara NoIR (Camera Module 2 NoIR) del AstroPi04.
- Las librerías OpenCV y NumPy.
- Los archivos
ndvi.pyyfastiecm.py(el mapa de color de la guía) en~/astropi/astropi10. - Las fotos de prueba
park.png(de la guía) eiss146.jpg(de la Astro Pi Izzy, sacada de los recursos del proyecto en GitHub). - Un filtro azul, si lo tenéis. Nosotros no lo teníamos, y eso nos ha enseñado mucho.
Para instalar las librerías, abrid un terminal:
sudo apt update
sudo apt install python3-opencv python3-numpy libopenblas-dev
El simulador paso a paso
Antes de tocar la Raspberry Pi, practicad con el simulador del NDVI. Tiene 6 pasos: los canales de color, el contraste, el NDVI, el mapa de color, la foto sin filtro y la foto de la ISS. Podéis pulsar sobre cualquier píxel para ver sus números en cada paso.

El código
Este es nuestro ndvi.py. Es el código de la guía oficial con tres cambios: guarda los archivos junto al programa (con __file__, como en Mission Space Lab), sigue aunque cierres una ventana con la X y recorta los valores con np.clip antes de colorear.
# Chisme del Lince AstroPi10 - NDVI: plant health from space
# Based on "Capture plant health with NDVI and Raspberry Pi"
# (Raspberry Pi Foundation, CC BY-SA 4.0)
import cv2
import numpy as np
from pathlib import Path
from fastiecm import fastiecm
folder = Path(__file__).parent # the folder where ndvi.py is
# Choose the image: 'park.png' (guide example), 'iss146.jpg' (ISS)
# or 'camera' to take a new photo with the NoIR camera
SOURCE = 'park.png'
if SOURCE == 'camera':
from picamzero import Camera
cam = Camera()
cam.still_size = (1920, 1080) # NoIR camera (Camera Module 2)
cam.take_photo(str(folder / 'original.jpg'))
original = cv2.imread(str(folder / 'original.jpg'))
else:
original = cv2.imread(str(folder / SOURCE))
def display(image, image_name):
image = np.array(image, dtype=float) / float(255)
shape = image.shape
height = int(shape[0] / 2)
width = int(shape[1] / 2)
image = cv2.resize(image, (width, height))
cv2.namedWindow(image_name)
cv2.imshow(image_name, image)
# Wait until a key is pressed or the window is closed with the X
while cv2.getWindowProperty(image_name, cv2.WND_PROP_VISIBLE) >= 1:
if cv2.waitKey(100) != -1:
break
cv2.destroyAllWindows()
def contrast_stretch(im):
in_min = np.percentile(im, 5) # darkest 5 %
in_max = np.percentile(im, 95) # brightest 5 %
out_min = 0.0
out_max = 255.0
out = im - in_min
out *= ((out_min - out_max) / (in_min - in_max))
out += in_min
return out
def calc_ndvi(image):
b, g, r = cv2.split(image) # OpenCV keeps colours as B, G, R
bottom = (r.astype(float) + b.astype(float))
bottom[bottom == 0] = 0.01 # never divide by zero
ndvi = (b.astype(float) - r) / bottom
return ndvi
display(original, 'Original')
contrasted = contrast_stretch(original)
display(contrasted, 'Contrasted original')
cv2.imwrite(str(folder / 'contrasted.png'), contrasted)
ndvi = calc_ndvi(contrasted)
display(ndvi, 'NDVI')
ndvi_contrasted = contrast_stretch(ndvi)
display(ndvi_contrasted, 'NDVI Contrasted')
cv2.imwrite(str(folder / 'ndvi_contrasted.png'), ndvi_contrasted)
# Keep values between 0 and 255 before turning them into whole numbers
color_mapped_prep = np.clip(ndvi_contrasted, 0, 255).astype(np.uint8)
color_mapped_image = cv2.applyColorMap(color_mapped_prep, fastiecm)
display(color_mapped_image, 'Color mapped')
cv2.imwrite(str(folder / 'color_mapped_image.png'), color_mapped_image)
print('Done! Images saved in', folder)
El código, función a función
cv2.imreadcarga la foto como una tabla de números. Ojo: OpenCV guarda los colores en orden B, G, R (azul, verde, rojo), no R, G, B.contrast_stretchbusca el valor del 5 % más oscuro y del 5 % más claro connp.percentiley estira esa franja hasta 0-255. La foto gana contraste.calc_ndvisepara los canales concv2.splity calcula(b − r) / (b + r)en cada píxel. La líneabottom[bottom == 0] = 0.01evita dividir entre cero.applyColorMapcambia cada gris por un color del mapa fastie: blanco y gris para lo más bajo; azul, verde, amarillo, rojo y rosa para lo más alto.
¿Por qué la guía calcula b − r y no r − b? En la foto del parque, la luz infrarroja de las plantas ha quedado en el canal que OpenCV llama b: por eso la vegetación sale azul cian. Comprobadlo en el paso 1 del simulador: en un árbol, b ≈ 160 y r ≈ 75.
Dos tropiezos que os vais a encontrar
La ventana que no se cierra. El código original usa cv2.waitKey(0), que espera una tecla. Si cerráis la ventana con la X, el programa se queda esperando sin ventana. Nuestro display comprueba también si la ventana sigue abierta.
El aviso de NumPy. Con el código original nos salió RuntimeWarning: invalid value encountered in cast. El estiramiento deja fuera de 0-255 a cerca del 10 % de los píxeles, y astype(np.uint8) no sabe convertirlos. La solución es una línea: np.clip(ndvi_contrasted, 0, 255) deja lo menor que 0 en 0 y lo mayor que 255 en 255.
Si Thonny no se abre por Escritorio remoto, probad a lanzarlo desde el terminal con thonny: puede que haya otro Thonny abierto en la pantalla de la Pi.
Cómo probarlo
- Ejecutad
ndvi.pyconSOURCE = 'park.png'. Salen 5 ventanas; pulsad una tecla para pasar a la siguiente. - Cambiad a
SOURCE = 'iss146.jpg'y repetid. - Cambiad a
SOURCE = 'camera', apuntad a unas plantas con suelo o pared alrededor y ejecutadlo. - Abrid
color_mapped_image.pngen la carpeta y comparad los tres resultados.
Qué medimos realmente con el NDVI de la Raspberry Pi
Con filtro azul: funciona
En la foto del parque, los árboles y el césped salen amarillos, rojos y rosas, y el asfalto y las casas, blancos y grises. Un árbol da un NDVI positivo; el asfalto, negativo. Es lo que esperábamos.
Sin filtro azul: parece que funciona, pero no
Esta es la foto de la terraza del Lince, con la NoIR sin filtro. La original sale roja: el infrarrojo entra por los tres canales, sobre todo por el rojo (de media, r = 175 y b = 79).

El mapa de color parece un NDVI, pero mirad los números: en la foto sin tocar, el NDVI va de −0,64 a −0,16 en el 90 % de los píxeles. Casi todo es negativo, las hojas también.
¿Por qué sale entonces tan colorido? Porque contrast_stretch estira lo que haya: pinta de rojo lo menos negativo, no lo más sano. Por eso hay hojas sanas en gris y una alfombrilla en verde. Medir no significa medir bien.
Desde la ISS: cuidado con lo que no es Tierra
La foto de la Astro Pi Izzy muestra desierto y nubes. El NDVI sale cerca de 0, que es lo correcto: no hay vegetación.

Pero el borde de la ventana y el marco de la nave salen en rojo y rosa. El programa no sabe qué es Tierra y qué es la ISS. En un experimento de verdad habría que recortar la ventana y elegir fotos sin nubes y con vegetación.
El NDVI de la Raspberry Pi es relativo
Por culpa de contrast_stretch, los colores comparan los píxeles dentro de cada foto. Un mismo rojo en dos fotos distintas no significa el mismo NDVI. Para comparar fotos (por ejemplo, el mismo bosque en dos fechas) hay que usar el NDVI sin estirar, que está en la variable ndvi.
Para el aula
- Predice: antes de ejecutar, ¿qué color tendrá el asfalto? ¿Y el cielo? Comprobadlo en el simulador pulsando sobre ellos.
- Busca el error: ejecutad el código original de la guía (con
astypesinnp.clip). ¿Qué aviso sale? ¿Qué porcentaje de píxeles lo provoca? - Compara: misma planta con luz del sol y con luz de una lámpara LED. ¿Cambia el NDVI? ¿Por qué? (Pista: ¿emiten infrarrojo los LED?)
- Diseña: ¿qué foto haríais para comprobar que una planta regada tiene más NDVI que una sin regar? ¿Qué tendría que ser igual en las dos fotos?
Retos
Reto 1
Haz que funcione con las tres fuentes: el parque, la ISS y tu cámara.
Reto 2
Imprime la media de ndvi (sin estirar) con print(np.mean(ndvi)). Compara la del parque, la de la ISS y la de tu foto.
Reto 3
Consigue un filtro azul (vale un gel de teatro azul) y repite la foto de tus plantas. ¿Cambia el signo del NDVI?
Reto 4
Recorta la foto de la ISS para quedarte solo con el centro de la ventana y calcula el NDVI de ese trozo.
Reto 5
Haz un porcentaje de vegetación: cuenta los píxeles con NDVI mayor que un umbral y divide entre el total.
Reto 6
Prueba otras fotos de la ISS de los recursos de la guía y busca alguna con campos o bosques.
Reto 7
Convierte el programa en un experimento de Mission Space Lab: una foto cada cierto tiempo, el NDVI medio en un CSV y un máximo de 42 imágenes.
Qué hemos aprendido
- El NDVI compara el infrarrojo cercano y el rojo para medir la salud de las plantas, como hacen los satélites.
- La cámara NoIR con un filtro azul permite calcular el NDVI con Raspberry Pi.
- OpenCV guarda los colores como B, G, R, y
np.percentile,np.clipyapplyColorMapson herramientas de procesado de imagen muy útiles. - Sin el filtro adecuado, el programa da un mapa bonito pero engañoso: una imagen bonita no es una medida.
Siguiente paso: del dato a la pregunta. ¿Qué queremos investigar con todo lo que ya sabemos medir?
Para seguir investigando
- AstroPi04: la cámara NoIR de la Raspberry Pi
- AstroPi09: la velocidad de la ISS con dos fotos
- Guía oficial: Capture plant health with NDVI and Raspberry Pi
- NDVI en Wikipedia
- Mission Space Lab (Astro Pi)

Etiqueta:AIDARAC, Astro Pi, cámara NoIR, ies monterroso, ISS, Mission Space Lab, montesteam, ndvi, NumPy, OpenCV, python, Raspberry Pi



