
Chisme del Lince AstroPi11: del dato a la pregunta, diseña tu experimento Astro Pi
La mesa de Víctor Vílchez, el Lince de la Noche, parece un laboratorio tras una tormenta: la Raspberry Pi, la cámara NoIR, la Sense HAT y un cuaderno con una lista enorme de cosas que quiere medir. El campo magnético, la velocidad de la ISS, las plantas, las nubes, la temperatura…
—Voy a medirlo todo —anuncia.
—Tienes diez minutos de luz, como mucho cuarenta y dos fotos y una sola oportunidad —responde la Gata Montesa—. ¿Qué quieres averiguar?
—Todo.
—«Todo» no es una pregunta. Es una lista de la compra.
El Lince tacha cinco renglones con cara de dolor y deja uno: ¿cuánta vegetación hay en cada foto?
—Ahora sí tienes un experimento. Y antes de escribir una línea de Python vamos a dibujar qué hará el programa.
—¿Dibujar? Yo pensaba empezar por el teclado.
—Los programas que salen bien se piensan antes de escribirse. Y los que salen mal, también se piensan: después.
Introducción
Hasta ahora hemos aprendido a medir: los sensores (AstroPi03), la cámara (AstroPi04), el tiempo (AstroPi06), la velocidad de la ISS (AstroPi09) y la salud de las plantas (AstroPi10).
Hoy toca lo que va antes de medir: diseñar un experimento Astro Pi. Seguimos el proceso de diseño de la guía del mentor de Mission Space Lab (Stage 3) y lo probamos con fotos reales de la ISS y con la cámara NoIR de nuestra terraza.
Por qué este chisme
Un equipo de Mission Space Lab tiene que decidir qué investiga, qué datos necesita y cómo comprobará que su programa funciona, porque en la ISS nadie podrá arreglarlo. Un buen experimento Astro Pi empieza por una pregunta y por un plan, no por el código.
Además, en este chisme pasa algo que no esperábamos: probar con la cámara real encontró un fallo que las fotos de prueba no tenían. Eso también es diseñar.
El proceso de diseño en cinco palabras
La guía del mentor propone el design thinking, un método que se resume en cinco pasos:
- Empatizar: ¿quién usa el programa? Se ejecuta solo, lanzado desde tierra, y nadie estará allí para corregirlo.
- Definir: qué vamos a investigar, qué datos hacen falta y cómo parará el programa a tiempo, con un archivo de salida.
- Idear: pensar varias ideas, compararlas y descartar las que no se pueden hacer.
- Prototipar: probar el algoritmo con datos ya guardados, como fotos históricas, y después con la cámara.
- Probar: repasar el programa con el Astro Pi Replay Tool y comprobar las normas.
Paso 1: una pregunta que se pueda comprobar
Una pregunta sirve como experimento si tiene variable, unidad e instrumento, cabe en 10 minutos de luz y puede salir «no» o «casi nada». Comparad tres preguntas:
- ¿Es bonita la Tierra vista desde la ISS? No se puede comprobar: «bonita» no es una variable y dos personas darían dos respuestas.
- ¿Hace calor dentro de la ISS? Se puede medir, pero casi no cambia en 10 minutos, así que no habría nada que comparar (lo vimos en el AstroPi03).
- ¿Qué porcentaje de cada foto es vegetación? Sí: la variable son los píxeles con NDVI mayor que 0,2, la unidad es el % y el instrumento es la cámara NoIR.
Nuestro experimento Astro Pi es la tercera pregunta: la idea del NDVI convertida en un programa que trabaja solo.
Paso 2: los bloques y el diagrama de flujo
Para responder, el programa hace ocho cosas: preparar la cámara y el CSV, mirar si queda tiempo, hacer una foto, comprobar si es buena, medir la vegetación, guardar una fila, esperar unos segundos y, al final, cerrar el archivo.
Con ellas se dibuja un diagrama de flujo: inicio, final, decisiones con sus dos salidas y la vuelta que se repite durante 10 minutos. No lleva código, así que cualquiera del equipo puede entenderlo.

Los tres bloques naranja cumplen una norma de la ISS: terminar antes de 10 minutos, hacer como máximo 42 fotos y guardar un archivo permitido en la carpeta del programa. Si no están en el diagrama, es fácil olvidarlas al programar.
El simulador paso a paso
Antes de tocar la Raspberry Pi, practicad con el simulador del AstroPi11. Tiene 7 pasos: la pregunta, el orden de los bloques, el diagrama, los «¿y si…?», las 13 fotos de prueba, las tomas de la terraza con la cámara real y el reparto de tareas.

Paso 3: ¿y si…?
La guía pide pensar en lo que puede salir mal, sobre todo dentro del bucle que se repite 10 minutos, porque un error sin controlar pararía la toma de datos. Nuestro programa prevé cuatro casos:
- ¿Y si la foto es de noche? Se descarta si el brillo es menor que 25 (de 255).
- ¿Y si una foto falla? Un
try / exceptanota una fila convalid = 0y el programa sigue. - ¿Y si no hay plantas? Un 0 % no es un error: es un resultado.
- ¿Y si se acaba el tiempo? El bucle deja de empezar fotos 10 segundos antes del límite.
El código
Este es nuestro main.py. Cada función es un bloque del diagrama y main() es el «esqueleto» que las llama en orden. Guardadlo en ~/astropi/astropi11, con una carpeta fotos al lado que contenga las fotos de prueba. Para ahorraros trabajo, descargad la carpeta ya preparada (zip).
Lo único que debéis cambiar está al principio, en los ajustes del equipo. Con SOURCE = 'photos' usa las fotos de la carpeta; con SOURCE = 'camera' usa la cámara.
# Chisme del Lince AstroPi11 - From data to question: the experiment skeleton
#
# Our question: what percentage of each photo shows healthy vegetation (NDVI)?
#
# Every function below is ONE BLOCK of the flow chart.
# The main() function at the end is the "scaffold": it only calls the
# blocks in the right order. Whoever writes main() needs to know all blocks.
#
# Based on the NDVI guide of the Raspberry Pi Foundation (CC BY-SA 4.0).
import csv
from datetime import datetime, timedelta
from pathlib import Path
from time import sleep
import cv2
import numpy as np
# ---------------------------------------------------------------------
# TEAM SETTINGS: change them here, nowhere else
# ---------------------------------------------------------------------
SOURCE = 'photos' # 'photos' = static test photos, 'camera' = live camera
RUN_MINUTES = 1 # 10 on the ISS
SAFETY_SECONDS = 10 # stop a little early: a photo needs about 2 seconds
PHOTO_EVERY = 12 # seconds between photos (camera mode)
MAX_PHOTOS = 42 # Mission Space Lab limit
MIN_BRIGHTNESS = 25 # darker photos are night or errors: do not trust them
MAX_SATURATED = 0.8 # if more than 80 % of the pixels are saturated (covered lens?)
NDVI_THRESHOLD = 0.2 # pixels above this count as vegetation
folder = Path(__file__).parent # the folder where main.py is
# ---------------------------------------------------------------------
# BLOCK 1 - get a photo
# ---------------------------------------------------------------------
def photo_from_folder(photo_file):
"""Static test: read a photo that is already saved."""
return cv2.imread(str(photo_file))
def photo_from_camera(camera, number):
"""Live: take a photo with the camera and read it back."""
photo_file = folder / f'image_{number:03d}.jpg'
camera.take_photo(str(photo_file))
return cv2.imread(str(photo_file))
# ---------------------------------------------------------------------
# BLOCK 2 - look only at the middle (the edge of the window is not Earth)
# ---------------------------------------------------------------------
def central_crop(image):
height, width = image.shape[:2]
return image[int(height * 0.25):int(height * 0.75),
int(width * 0.30):int(width * 0.70)]
# ---------------------------------------------------------------------
# BLOCK 3 - is the photo good enough? (the "what if...?" blocks)
# ---------------------------------------------------------------------
def get_brightness(image):
return float(central_crop(image).mean())
def get_saturated(image):
"""Fraction of pixels (0 to 1) where some colour channel is at its maximum."""
crop = central_crop(image)
return float((crop.max(axis=2) >= 250).mean())
# ---------------------------------------------------------------------
# BLOCK 4 - measure: NDVI of every pixel and percentage of vegetation
# ---------------------------------------------------------------------
def measure_vegetation(image):
crop = central_crop(image).astype(float)
b = crop[:, :, 0] # OpenCV order: B, G, R
r = crop[:, :, 2]
bottom = b + r
bottom[bottom == 0] = 0.01 # never divide by zero
ndvi = (b - r) / bottom # NDVI without stretching the contrast
vegetation_percent = float((ndvi > NDVI_THRESHOLD).mean() * 100)
ndvi_median = float(np.median(ndvi))
return vegetation_percent, ndvi_median
# ---------------------------------------------------------------------
# BLOCK 5 - save one row of the CSV file
# ---------------------------------------------------------------------
def save_row(writer, name, brightness, saturated, valid, vegetation_percent, ndvi_median):
writer.writerow([
datetime.now().isoformat(timespec='milliseconds'),
name,
round(brightness, 1),
round(saturated, 3),
valid,
'' if vegetation_percent is None else round(vegetation_percent, 2),
'' if ndvi_median is None else round(ndvi_median, 3),
])
# ---------------------------------------------------------------------
# SCAFFOLD - calls the blocks in the right order
# ---------------------------------------------------------------------
def main():
start = datetime.now()
end = start + timedelta(minutes=RUN_MINUTES, seconds=-SAFETY_SECONDS)
camera = None
files = []
if SOURCE == 'camera':
from picamzero import Camera
camera = Camera() # create it ONCE
camera.still_size = (1920, 1080)
else:
files = sorted((folder / 'fotos').glob('*.jpg'))
csv_path = folder / 'data.csv'
print('Saving data in:', csv_path)
with open(csv_path, 'w', newline='', buffering=1) as csv_file:
writer = csv.writer(csv_file)
writer.writerow(['time', 'photo', 'brightness', 'saturated', 'valid',
'veg_percent', 'ndvi_median'])
number = 0
valid_photos = 0
while datetime.now() < end and number < MAX_PHOTOS:
name = '?'
try:
if camera is not None:
name = f'image_{number + 1:03d}.jpg'
image = photo_from_camera(camera, number + 1)
else:
if number >= len(files):
break # no more test photos
name = files[number].name
image = photo_from_folder(files[number])
number += 1
brightness = get_brightness(image)
saturated = get_saturated(image)
if brightness < MIN_BRIGHTNESS:
# WHAT IF the photo is dark? The numbers would be noise.
save_row(writer, name, brightness, saturated, 0, None, None)
print(f'{name}: too dark ({brightness:.1f}), not valid')
elif saturated > MAX_SATURATED:
# WHAT IF the lens is covered? The photo is bright but empty.
save_row(writer, name, brightness, saturated, 0, None, None)
print(f'{name}: saturated ({saturated * 100:.0f} %), not valid')
else:
percent, median = measure_vegetation(image)
save_row(writer, name, brightness, saturated, 1, percent, median)
valid_photos += 1
print(f'{name}: vegetation {percent:.2f} % '
f'(NDVI median {median:.3f})')
except Exception as error:
# WHAT IF something fails? Write it down and keep going.
number += 1
print(f'{name}: ERROR ({error})')
writer.writerow([datetime.now().isoformat(timespec='milliseconds'),
name, '', '', 0, '', ''])
if camera is not None:
left = (end - datetime.now()).total_seconds()
sleep(max(0, min(PHOTO_EVERY, left)))
print(f'Photos: {number}, valid: {valid_photos}, not valid: {number - valid_photos}')
print('Done! Your file is here:', csv_path)
main()
El código, función a función
photo_from_folderyphoto_from_cameraconsiguen la foto: una ya guardada o una nueva de la cámara, que se crea una sola vez.central_cropse queda con el centro de la imagen, porque el borde de la ventana de la ISS no es Tierra.get_brightnessyget_saturateddeciden si la foto es de fiar. Las veremos trabajar enseguida.measure_vegetationcalcula el NDVI de cada píxel con(b − r) / (b + r)y devuelve el porcentaje que supera el umbral.save_rowescribe una fila del CSV. Conbuffering=1cada fila se guarda al instante, aunque el programa se corte.
Cómo probarlo
- Ejecutad
main.pyconSOURCE = 'photos'. Debe terminar conPhotos: 13, valid: 9, not valid: 4. - Cambiad a
SOURCE = 'camera', apuntad a unas plantas durante 1 minuto (salen unas 4 fotos) y ejecutadlo. - Guardad cada toma en su carpeta antes de repetir, porque
data.csvy las fotos se sobrescriben:mkdir plantasymv image_*.jpg data.csv plantas/.
Qué medimos realmente con el experimento Astro Pi
El control positivo funciona
La foto del parque, que usamos como control positivo, da un 76,7 % de vegetación. Las 9 fotos de la ISS de día dan entre 0 y 0,9 %. Si el programa no viera plantas en el parque, sabríamos que algo falla.
En esas fotos hay desierto, nubes y mar, así que casi 0 % es la respuesta correcta. Un 0 % también es un dato.
La noche engaña
Una foto de noche (la 22) no tiene plantas, pero si no comprobamos el brillo el programa mide un 98,9 % de vegetación. Es ruido de la cámara, y parecería un descubrimiento.
De las 250 fotos de la guía, 114 (46 %) son de noche. Y en las demás, ninguna pasa del 1,4 % de vegetación.
Sin filtro azul, el NDVI sale negativo
Con la cámara NoIR de la terraza, sin filtro, las plantas sanas dieron un NDVI mediana de −0,34 y un 0 % de vegetación. Ya lo vimos en el AstroPi10: la fórmula está pensada para una cámara con filtro azul.
Probamos también con una bolsa de congelador poco azul delante: el NDVI subió a −0,12. Mejora, pero sigue negativo. Es una prueba casera y no controlada (cambiaron el encuadre y la luz), así que sirve para ver el efecto, no para medirlo.

La cámara tapada no es oscura
Tapamos el objetivo para simular la noche, y esperábamos fotos oscuras. Salió otra cosa: una imagen violeta y muy brillante, con el canal azul al máximo. El brillo medio fue de 115 (de 255), así que la primera comprobación la dio por buena y midió un 100 % de vegetación.
No sabemos con certeza por qué. Nuestra hipótesis es que lo que cubre el objetivo deja pasar infrarrojo y la cámara NoIR, que no tiene filtro, lo ve.
La solución fue un bloque nuevo: get_saturated calcula qué fracción de los píxeles tiene un canal al máximo (99 % en la toma tapada). Si pasa del 80 %, la foto no es válida. En las 136 fotos de día de la ISS, ninguna pasa del 1 %, así que el control nuevo no descarta fotos buenas.
Dato no es lo mismo que medida
Con las fotos de prueba, el programa parecía perfecto. Fue la cámara real la que encontró el hueco. Por eso la guía insiste en repetir el ciclo: prototipar, probar, mejorar el diseño y volver a probar.
Paso 4: el reparto de tareas
Cada bloque del diagrama es una función, y se reparten entre el equipo según experiencia y ganas. La guía pide que una persona sea responsable de main(), el esqueleto que las llama en el orden correcto. Cada una prueba su bloque sola, con las fotos de la carpeta.
Y si el plan cambia mientras se programa, se actualiza el diagrama. En nuestro caso cambió: apareció la comprobación de saturación.
Para el aula
- Predice: antes de ejecutar, ¿qué porcentaje saldrá en el parque? ¿Y en la foto 22 si quitamos la comprobación del brillo?
- Busca el error: poned
MIN_BRIGHTNESS = 0yMAX_SATURATED = 1. ¿Qué fotos se cuelan como válidas? - Cambia una variable: probad
NDVI_THRESHOLD = 0.1y0.3. ¿Cambia el porcentaje del parque? ¿Y el de las fotos de la ISS? - Diseña: escribid tres preguntas de investigación y decidid cuáles se pueden comprobar con vuestros sensores en 10 minutos.
- Controles: ¿qué control positivo usaríais para un experimento con el magnetómetro?
Retos
Reto 1
Haz que funcione con las 13 fotos y compara tu data.csv con el nuestro.
Reto 2
Cambia NDVI_THRESHOLD y representa cómo cambia el porcentaje de vegetación del parque.
Reto 3
Haz tus propias tomas con la cámara, con y sin una bolsa azul, a pleno sol y desde la misma posición. ¿Cambia el signo del NDVI?
Reto 4
Dibuja el diagrama de flujo de otro experimento, por ejemplo uno con el magnetómetro, sin escribir código.
Reto 5
Carga tu data.csv en el simulador del AstroPi05 y dibuja una gráfica.
Reto 6
Prueba el programa en el Astro Pi Replay Tool del AstroPi07 y cuéntanos si OpenCV funciona allí. Nosotros todavía no lo hemos comprobado.
Reto 7
Escribe la pregunta de investigación de tu equipo y su diagrama de flujo, con las normas de la ISS marcadas.
Qué hemos aprendido
- Un experimento Astro Pi empieza por una pregunta con variable, unidad e instrumento, y que pueda salir «no».
- Antes del código se dibuja un diagrama de flujo, con las decisiones, la vuelta y las normas de la ISS marcadas.
- Hay que pensar los «¿y si…?»: de noche, un fallo, sin plantas, sin tiempo.
- Un control positivo nos dice si el programa funciona.
- Probar con datos reales encontró un fallo (la cámara tapada) que las fotos de prueba no tenían: medir no significa medir bien.
Siguiente paso: el programa ya tiene su diseño. Falta repasar el reglamento punto por punto antes de enviarlo.
Para seguir investigando
- AstroPi10: NDVI, la salud de las plantas desde el espacio
- AstroPi07: tu programa viaja a la ISS (casi)
- Guía del mentor de Mission Space Lab, Stage 3: el proceso de diseño
- Guía oficial para crear tu programa (creator guide)
- Capture plant health with NDVI and Raspberry Pi
- Diagrama de flujo en Wikipedia

Etiqueta:Astro Pi, design thinking, diagrama de flujo, Mission Space Lab, ndvi, OpenCV, python, Raspberry Pi



