
Entrada 5: Proyecto BermejaPi — Estructura de Vuelo de 10 Minutos (main.py)
Una vez configurado nuestro entorno de desarrollo en Raspberry Pi OS, es momento de construir la arquitectura del programa principal de vuelo (main.py), ajustándonos rigurosamente a las normas impuestas por la ESA (European Space Agency) para la competición Astro Pi Mission Space Lab.
1. Preparación de Dependencias e Instalación de OpenCV
Para garantizar que los paquetes de visión por computador se instalen sin errores de repositorios desfasados en la máquina virtual, preparamos la consola e instalamos OpenCV y PIL:
Bash
# 1. Ajustar repositorios de la máquina virtual e instalar paquetes de visión
sudo sed -i 's/^deb http:\/\/deb.debian.org\/debian-security/#deb http:\/\/deb.debian.org\/debian-security/' /etc/apt/sources.list
sudo apt update
sudo apt install -y python3-opencv python3-pil
# 2. Verificar que OpenCV se ha instalado correctamente
python3 -c "import cv2; print('OpenCV versión:', cv2.__version__)"
2. Normas de Vuelo de la ESA (Astro Pi Rulebook)
El script de vuelo debe cumplir cuatro reglas fundamentales a bordo de la Estación Espacial Internacional (ISS):
- Tiempo estricto de 10 minutos (600 segundos): El programa debe detenerse ordenadamente antes de alcanzar este límite.
- Ejecución desatendida (Headless): Queda prohibida cualquier interfaz gráfica (
cv2.imshow(),plt.show()) o peticiones de entrada de teclado (input()). - Límite de almacenamiento (250 MB): Todos los archivos generados (imágenes temporales y
.csv) deben mantenerse bajo esta cuota. - Manejo de excepciones: El bucle principal debe capturar cualquier error inesperado para evitar caídas catastróficas durante el vuelo.
3. Código Fuente Principal (main.py)
Crea o actualiza el archivo main.py en la carpeta de tu proyecto con la siguiente estructura completa:
Python
import csv
import time
from datetime import datetime, timedelta
from pathlib import Path
import cv2
import numpy as np
# Cargar módulo oficial de cámara si existe hardware real
try:
from picamera2 import Picamera2
except ImportError:
Picamera2 = None
# ============================================
# CONSTANTES DE MISIÓN Y FÍSICA
# ============================================
DIR_BASE = Path(__file__).parent.resolve()
FICHERO_CSV = DIR_BASE / "data.csv"
DURACION_MINUTOS = 10
TIEMPO_LIMITE_SEG = DURACION_MINUTOS * 60 # 600 segundos
INTERVALO_CAPTURA_SEG = 10 # Intervalo entre tomas
ALTURA_ISS_M = 400000.0 # Altura promedio de la ISS (400 km)
ANCHO_SENSOR_MM = 7.564 # Especificaciones del sensor HQ Camera
FOCAL_MM = 6.0
ANCHO_IMAGEN_PX = 4056
def inicializar_csv():
"""Crea el archivo data.csv con las cabeceras requeridas si no existe."""
if not FICHERO_CSV.exists():
with open(FICHERO_CSV, mode="w", newline="", encoding="utf-8") as f:
escritor = csv.writer(f)
escritor.writerow(["timestamp", "delta_t_s", "desplazamiento_px", "velocidad_kms"])
def capturar_imagen(ruta, camara=None):
"""Captura foto con la cámara o genera terreno sintético si no hay hardware."""
if camara is not None:
camara.capture_file(str(ruta))
else:
# Generación sintética para pruebas en máquina virtual
h, w = 1080, 1920
terreno = np.random.randint(80, 180, (h, w), dtype=np.uint8)
terreno = cv2.GaussianBlur(terreno, (31, 31), 0)
cv2.circle(terreno, (500, 500), 100, (230), -1)
cv2.imwrite(str(ruta), terreno)
def calcular_velocidad(ruta1, ruta2, delta_t):
"""Calcula el desplazamiento y velocidad orbital mediante puntos clave ORB en OpenCV."""
img1 = cv2.imread(str(ruta1), cv2.IMREAD_GRAYSCALE)
img2 = cv2.imread(str(ruta2), cv2.IMREAD_GRAYSCALE)
if img1 is None or img2 is None:
return 0.0, 0.0
orb = cv2.ORB_create(nfeatures=1000)
kp1, des1 = orb.detectAndCompute(img1, None)
kp2, des2 = orb.detectAndCompute(img2, None)
if des1 is None or des2 is None:
return 0.0, 0.0
bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
coincidencias = bf.match(des1, des2)
if not coincidencias:
return 0.0, 0.0
coincidencias = sorted(coincidencias, key=lambda x: x.distance)[:50]
distancias_px = []
for m in coincidencias:
pt1 = kp1[m.queryIdx].pt
pt2 = kp2[m.trainIdx].pt
dist_px = np.sqrt((pt2[0] - pt1[0])**2 + (pt2[1] - pt1[1])**2)
distancias_px.append(dist_px)
desplazamiento_px = float(np.mean(distancias_px))
# Tamaño de píxel sobre el terreno (Ground Sample Distance)
gsd = (ALTURA_ISS_M * ANCHO_SENSOR_MM) / (FOCAL_MM * ANCHO_IMAGEN_PX)
distancia_m = desplazamiento_px * gsd
velocidad_kms = (distancia_m / delta_t) / 1000.0 if delta_t > 0 else 0.0
return desplazamiento_px, velocidad_kms
def registrar_telemetria(delta_t, desplazamiento_px, velocidad_kms):
"""Guarda una nueva entrada en el registro de datos CSV."""
ahora = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
with open(FICHERO_CSV, mode="a", newline="", encoding="utf-8") as f:
escritor = csv.writer(f)
escritor.writerow([ahora, round(delta_t, 2), round(desplazamiento_px, 2), round(velocidad_kms, 2)])
def bucle_principal_mision():
print("=== INICIANDO SECUENCIA OFICIAL BERMEJAPI (10 MIN) ===")
inicializar_csv()
camara = None
if Picamera2 is not None:
try:
camara = Picamera2()
camara.start()
except Exception as e:
print(f"Cámara no detectada. Modo simulación activo: {e}")
hora_inicio = datetime.now()
# Margen de seguridad de 10 segundos antes del límite de 600s
hora_fin = hora_inicio + timedelta(seconds=TIEMPO_LIMITE_SEG - 10)
foto_anterior = DIR_BASE / "foto_a.jpg"
foto_actual = DIR_BASE / "foto_b.jpg"
# Captura inicial previa al bucle
capturar_imagen(foto_anterior, camara)
tiempo_foto_anterior = time.time()
ciclo = 1
while datetime.now() < hora_fin:
try:
print(f"\n--- Ciclo de ejecución {ciclo} ---")
time.sleep(INTERVALO_CAPTURA_SEG)
capturar_imagen(foto_actual, camara)
tiempo_foto_actual = time.time()
delta_t = tiempo_foto_actual - tiempo_foto_anterior
px, v_kms = calcular_velocidad(foto_anterior, foto_actual, delta_t)
print(f"Delta t: {delta_t:.1f}s | Desplazamiento: {px:.1f}px | Velocidad: {v_kms:.2f} km/s")
registrar_telemetria(delta_t, px, v_kms)
# Sobrescribir la foto anterior para no acumular almacenamiento innecesario
foto_anterior.write_bytes(foto_actual.read_bytes())
tiempo_foto_anterior = tiempo_foto_actual
ciclo += 1
except Exception as e:
print(f"Excepción controlada en ciclo {ciclo}: {e}")
time.sleep(2)
if camara is not None:
camara.stop()
print("\n=== MISIÓN COMPLETADA DE FORMA LIMPIA Y SEGURA ===")
if __name__ == "__main__":
bucle_principal_mision()
4. Prueba y Verificación de Resultados
Ejecuta el script desde la Terminal dentro de tu carpeta de trabajo:
Bash
python3 main.py
Comprueba en cualquier momento que los datos se guardan secuencialmente abriendo el archivo data.csv:
Bash
cat data.csv
Etiqueta:Astro Pi, BermejaPi, ESA, main.py, Mission Space Lab, OpenCV, ORB, Raspberry Pi OS, Telemetría CSV, visión por computador



