- Diseño y desarrollo de un entorno de fotogrametría con drones asistido por IA
- Aplicación Android para fotogrametría con IA. Verificación del entorno y Fase 1 de desarrollo inicial
- Aplicación Android para fotogrametría con IA. Fase 2: Primera versión capaz de volar
- Aplicación Android para fotogrametría con IA. Fase 3: Corrección de heading, visualización en vuelo y ajustes de UX
- Aplicación Android para fotogrametría con IA. La plataforma de procesado: OpenDroneMap desplegado con Claude Code
- Aplicación Android para fotogrametría con IA. Fase 4: Integración cloud, exportación KMZ y cierre del ciclo completo
- Fotogrametría con IA: Detección de hallazgos en tiempo real durante el vuelo. Módulo de auditoría IA con YOLOv8 y RTMP sobre el stack ODM
Cuando empecé esta serie de artículos, el objetivo era construir una aplicación Android capaz de planificar y ejecutar misiones de fotogrametría con el DJI Mini 3 Pro. Ese objetivo está cumplido desde la Fase 2, y las fases siguientes han ido refinando el resultado. Pero hay algo que el flujo fotogramétrico clásico no puede hacer por su propia naturaleza: detectar algo mientras el dron está volando. Las fotos se procesan después de aterrizar, cuando el dron ya no está en el aire. Si hay una anomalía en el área inspeccionada —una grieta, un intruso, un panel solar averiado— el operador no lo sabrá hasta horas después, cuando el modelo 3D esté listo.
El módulo que describo en este artículo cierra esa carencia. Es una extensión del servidor de procesado que ya describí en el artículo sobre la plataforma ODM: añade un pipeline de vídeo en tiempo real que opera en paralelo al flujo fotogramétrico existente, sin interferir con él, y que permite detectar hallazgos durante el vuelo y georreferenciarlos automáticamente sobre el ortomosaico.

La idea: dos flujos paralelos desde el mismo dron
El planteamiento arquitectónico parte de una distinción clara entre dos tipos de datos que produce el dron simultáneamente: las fotografías de alta resolución, destinadas al procesado fotogramétrico diferido, y el stream de vídeo en tiempo real que el MSDK v5 expone continuamente desde la cámara. Hasta ahora, ese stream solo se usaba para la vista FPV en la pantalla del operador. El módulo de auditoría le da un segundo uso: lo ingesta en el servidor, lo analiza fotograma a fotograma con un modelo de detección de objetos, y persiste los hallazgos georreferenciados en una base de datos espacial.
Los dos flujos corren en paralelo y son completamente independientes:
DJI Mini 3 Pro
│
├── Fotos JPEG ─────────────────► WebODM (procesado diferido)
│ │
│ ▼
│ Ortomosaico / Nube de puntos
│
└── Stream vídeo H.264 ──► MediaMTX (RTMP) ──► YOLOv8
+ │
Telemetría GPS ──► audit-api ────────────► Hallazgo georreferenciado
(cada 500 ms) │
▼
Visor Leaflet + GeoJSON
sobre el ortomosaico
La clave de la georreferenciación es la sincronización por timestamp: la app Android estampa cada fotograma de vídeo con su propio reloj local y envía telemetría GPS con el mismo timestamp cada 500 ms. Cuando el procesador de IA detecta algo en un frame, consulta la base de datos para encontrar la posición GPS más cercana en tiempo, y esa coordenada se convierte en la ubicación geográfica del hallazgo. La latencia RTMP —típicamente 1 a 3 segundos— no afecta a la precisión porque la fuente de verdad temporal es siempre el reloj de la app, no el del servidor.
La arquitectura: tres contenedores nuevos sobre el stack existente
El módulo se despliega como un overlay de Docker Compose sobre el stack ODM que ya estaba funcionando. La decisión de diseño más importante fue no tocar nada del stack original: el overlay añade tres contenedores nuevos a la red Docker existente, modifica únicamente el nginx.conf para añadir tres bloques location, y comparte la base de datos PostgreSQL ya existente añadiendo cuatro tablas nuevas con IF NOT EXISTS para que el script sea completamente idempotente. Al terminar el despliegue, una verificación sistemática confirmó que todos los contenedores ODM preexistentes seguían funcionando exactamente igual, con sus datos intactos.
Red Docker interna: odm-stack_odm-net (172.20.0.0/16)
Puerto 80 (nginx — punto de entrada único)
│
├── / → WebODM UI (sin cambios)
├── /cluster/ → ClusterODM (sin cambios)
├── /3d/ → Potree (sin cambios)
└── /audit/ → audit-api (NUEVO)
Módulo de auditoría (overlay sobre el stack existente):
Dron ──RTMP──► ┌──────────────────┐ stream interno ┌──────────────────┐
│ audit-mediamtx │───────────────►│ audit- │
│ :1935 (RTMP) │ graba .mp4 │ ai-processor │
│ :8888 (HLS) │────────────────│ (YOLOv8m) │
│ :8889 (WebRTC) │ └────────┬─────────┘
└──────────────────┘ │ POST /detections
▼
App ──telemetría──► POST /audit/telemetry ──► ┌──────────────────────┐
│ audit-api │
│ (FastAPI :8001) │
└──────────┬───────────┘
│ SQL async
▼
odm-postgres (PostGIS)
tablas: flights,
drone_telemetry,
ai_detections,
odm_tasks
Cada contenedor tiene una responsabilidad bien acotada. MediaMTX recibe el stream RTMP del dron, lo redistribuye internamente para que el procesador de IA pueda consumirlo, y graba simultáneamente segmentos de 5 minutos en disco en formato MP4 sin recodificación, con retención automática de 30 días. audit-api es una API REST en FastAPI que gestiona los vuelos, recibe la telemetría GPS de la app, georreferencia los hallazgos y los expone como GeoJSON. audit-ai-processor lee el stream de MediaMTX, aplica YOLOv8 fotograma a fotograma y notifica a la API cuando detecta algo por encima del umbral de confianza.
El procesador de IA: YOLOv8 en CPU a 8 fps efectivos
El procesador de IA es el componente más interesante del módulo, y también el que más ajustes requirió. El DL360 Gen8 no tiene GPU, así que la inferencia se ejecuta íntegramente en CPU. YOLOv8 en CPU tarda unos 120 ms por frame con el modelo nano y unos 400 ms con el medium, sobre imágenes de 1080p. Procesar los 30 fps del stream es inviable; la solución es procesar uno de cada N frames, configurable mediante FRAME_SKIP. Con FRAME_SKIP=5 y el modelo medium se procesan efectivamente unos 8 fotogramas por segundo, suficiente para detectar objetos relativamente estáticos en una inspección aérea.
El modelo elegido fue yolov8m (medium) en lugar del nano original: tres veces más parámetros, notablemente mejor en objetos pequeños a distancia, que es el caso de uso habitual viendo el terreno desde 50-100 metros de altura. El umbral de confianza se fijó en 0.4, más permisivo que el 0.6 inicial, asumiendo que en inspección de infraestructuras es preferible revisar algún falso positivo antes que perder un hallazgo real.
La imagen Docker del procesador pasó por una optimización relevante: la versión inicial incluía PyTorch con soporte CUDA completo, que ocupa 9,6 GB. Como el servidor no tiene GPU, se sustituyó por la versión CPU-only, reduciendo la imagen de 9,6 GB a 3 GB. Un ahorro de 6,5 GB que importa cuando el disco raíz de la VM tiene 38 GB.
Re-análisis de vídeos grabados: una funcionalidad que surgió sola
Durante el desarrollo surgió una funcionalidad que no estaba en el planteamiento inicial pero que tiene mucho sentido: la posibilidad de re-analizar a posteriori los vídeos grabados por MediaMTX. El caso de uso es directo: si después de un vuelo se quiere aplicar un modelo diferente, bajar el umbral de confianza, o simplemente revisar un vuelo anterior con mayor detalle, no hace falta volver a volar. El servidor tiene grabado el vídeo completo.
El endpoint POST /audit/flights/{id}/reanalyze acepta opcionalmente un umbral de confianza distinto al por defecto, lanza el re-análisis en background y devuelve un job_id para consultar el estado. La lógica de inferencia se refactorizó en un módulo compartido (inference.py) entre el procesado en tiempo real y el re-análisis de ficheros.
Aquí apareció un bug interesante: los segmentos MP4 grabados por MediaMTX contienen frames H.264 corruptos intercalados, provocando que cv2.VideoCapture.read() falle en aproximadamente la mitad de las lecturas. El código original hacía break en el primer fallo, procesando en la práctica solo el primer frame del vídeo. La corrección fue saltar los frames fallidos y detener el procesado únicamente cuando hay más de 50 fallos consecutivos, que indica el final real del fichero.
PostGIS: la georreferenciación en una consulta
La base de datos espacial es el núcleo que une el procesado de vídeo con la cartografía. Las cuatro tablas añadidas usan tipos GEOMETRY nativos de PostGIS con índices espaciales GIST, lo que permite consultas de proximidad geográfica y temporal eficientes incluso con miles de puntos de telemetría por vuelo.
La georreferenciación de una detección se reduce a esto:
SELECT ST_X(geom::geometry) AS lon,
ST_Y(geom::geometry) AS lat
FROM drone_telemetry
WHERE flight_id = :flight_id
ORDER BY ABS(timestamp_ms - :detection_timestamp)
LIMIT 1
Una sola consulta que encuentra el punto GPS más cercano en tiempo al momento de la detección. Sencillo, pero correcto: el índice sobre timestamp_ms garantiza que sea rápido, y la lógica de sincronización por timestamp de la app garantiza que la coordenada resultante sea geográficamente precisa con independencia de la latencia de red.
El visor: hallazgos IA sobre el ortomosaico
El resultado final es accesible desde el navegador en /audit/view/{flight_id}. El visor Leaflet superpone dos capas: el ortomosaico GeoTIFF generado por ODM, cargado directamente en el navegador vía georaster-layer-for-leaflet sin necesidad de un servidor de tiles, y los hallazgos de IA como marcadores GeoJSON coloreados por confianza (rojo por encima del 85%, naranja el resto). Cada marcador abre un popup con clase detectada, porcentaje de confianza, timestamp y miniatura del frame anotado. El visor funciona también sin ortomosaico, mostrando los marcadores sobre OpenStreetMap mientras el procesado ODM todavía está en curso.

Claude Code como integrador de sistemas, de nuevo
Como en el despliegue del stack ODM base, Claude Code actuó aquí como ingeniero de sistemas autónomo. El proceso siguió el mismo patrón: un prompt detallado con el contexto del entorno existente, los componentes a desplegar en orden y los criterios de éxito para cada fase, ejecutado directamente en el servidor.

La restricción más importante que se impuso fue no tocar nada del stack existente. El prompt lo decía explícitamente, y la verificación final lo comprobó uno a uno: todos los servicios ODM preexistentes en el estado exacto en que estaban antes del despliegue, con sus datos intactos. La única modificación al stack original fue la adición de tres bloques location en nginx.conf.
Lo que resulta cada vez más evidente con el uso es que Claude Code no es solo una herramienta para generar código: es capaz de razonar sobre un sistema complejo existente, identificar los puntos de integración correctos y tomar decisiones coherentes con la arquitectura preexistente. La decisión de reutilizar la base de datos PostgreSQL existente añadiendo tablas nuevas en lugar de crear un contenedor separado, o la de usar el script DDL idempotente para que pueda re-ejecutarse sin riesgo, son el tipo de decisiones que toma alguien que entiende el sistema, no alguien que ejecuta instrucciones literalmente.
Lo que viene
Con este módulo desplegado, el servidor ofrece ya las dos capacidades del sistema completo: fotogrametría diferida con ODM y auditoría en tiempo real con YOLOv8. Lo que queda pendiente es conectar ambas desde la app Android: que la app inicie automáticamente el stream RTMP al comenzar una misión, envíe la telemetría al módulo de auditoría, y muestre las alertas de detección en la pantalla del operador durante el vuelo. Eso es el contenido de la Fase 5 de la app, pero eso quedará para un siguiente artículo de la serie.