- 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
- Aplicación Android para fotogrametría con IA. Fase 5: Streaming RTMP y telemetría en tiempo real — cómo se coordina un sistema entre dos agentes de IA
- Aplicación Android para fotogrametría con IA. Fase 6: Resultados en el mapa, gestión multi-batería y reanudación de misiones
Hasta la Fase 5, la aplicación era capaz de planificar y ejecutar misiones, enviar las imágenes a procesar y auditar el vuelo en tiempo real. Lo que le faltaba era cerrar el otro extremo del ciclo: mostrar los resultados dentro de la app, sin tener que abrir el navegador para ver el ortomosaico o los hallazgos IA. Y, en paralelo, dos limitaciones prácticas que cualquiera que haya hecho fotogrametría de campo conoce bien: qué pasa cuando la batería se acaba a mitad de misión, y cómo se continúa desde donde se dejó. La Fase 6 resuelve ambas cosas.
El ortomosaico como capa en el mapa
El resultado más visible de esta fase es poder ver el ortomosaico generado por ODM superpuesto directamente sobre el mapa de la aplicación. La implementación usa el servidor de tiles que WebODM expone para cada tarea procesada: una vez que el procesado termina, WebODM sirve el ortomosaico como tiles en coordenadas XYZ estándar, que MapLibre puede consumir como fuente raster.

La URL de los tiles tiene esta forma:
{serverUrl}/projects/{projectId}/tasks/{taskId}/orthophoto/tiles/{z}/{x}/{y}.png?jwt={token}
La autenticación JWT se renueva automáticamente antes de construir la URL, para evitar que los tiles fallen si el token ha caducado desde la última sesión. El toggle «Ortomosaico ODM» solo aparece en el selector de capas cuando la misión tiene un proyecto y tarea WebODM asociados, así que no ocupa espacio en la interfaz hasta que hay algo que mostrar.
Dos bugs de integración con MapLibre merecen mención porque son del tipo que no aparece en la documentación. El primero: las fuentes raster en MapLibre son inmutables tras su creación. Si se quiere cambiar la URL (por ejemplo, al cargar una misión diferente), no basta con actualizar la fuente; hay que eliminarla y crearla de nuevo. El segundo: el constructor de TileSet recibe como primer argumento la versión del esquema de tiles ("2.1.0"), no el nombre de la capa ni ningún otro identificador. Pasarle la URL de los tiles como primer argumento, que es lo que la lógica sugiere, produce un error silencioso.
El tercero fue encontrar la URL correcta: WebODM expone los tiles del ortomosaico en /orthophoto/tiles/, no en /tiles/. La diferencia de un segmento de path que no aparece documentada de forma clara y que se diagnosticó con curl directo contra el servidor hasta dar con la ruta que devolvía 200.
Los hallazgos IA en el mismo mapa
En paralelo al ortomosaico, la app carga también los hallazgos del módulo de auditoría IA directamente en el mapa como una capa GeoJSON. El endpoint /audit/project/{flight_id}/geojson del servidor devuelve una FeatureCollection con dos tipos de geometría: LineString para la trayectoria real del dron durante el vuelo, y Point para cada detección de YOLOv8.
MapLibre filtra las geometrías por tipo para asignarles capas distintas: la trayectoria se muestra como una línea verde sobre el mapa, y las detecciones como puntos rojos. Al tocar uno de esos puntos, queryRenderedFeatures() con una tolerancia de 20 píxeles devuelve los hallazgos cercanos al toque y muestra un popup con la clase del objeto detectado y el porcentaje de confianza.
El toggle «Hallazgos IA» del selector de capas incluye un contador con el número de detecciones del vuelo, calculado a partir del GeoJSON recibido. Las dos capas —ortomosaico y hallazgos— aparecen en el mismo selector de capas que ya existía para OSM, PNOA y Catastro, manteniendo una interfaz unificada para todo lo que puede mostrarse sobre el mapa.
Reanudación de misión interrumpida
El problema de las misiones interrumpidas es uno de esos que en el laboratorio parecen un detalle y sobre el terreno resultan críticos. El DJI Mini 3 Pro tiene unos 34 minutos de autonomía con la batería estándar que, en la práctica, se reducen a unos 25 minutos como máximo. Una misión de inspección de cierta superficie puede requerir más. Y aunque la batería aguante, cualquier incidencia —pérdida de señal, viento inesperado, necesidad de parar— puede interrumpir la misión a mitad.
La solución implementada es sencilla en concepto: persistir el índice del último waypoint completado en Room cada vez que el dron alcanza uno, de forma que al reabrir la app y cargar la misión, haya suficiente información para continuar desde donde se dejó.
Cuando una misión tiene progreso guardado, el botón de ejecución en la pantalla del mapa cambia de «Volar misión» a «Reanudar WP N», donde N es el waypoint pendiente. Al pulsar reanudar, el dron despega, asciende a la altitud de misión y navega directamente al waypoint pendiente, sin recorrer los anteriores. La barra de progreso en la pantalla de ejecución parte del waypoint N, no de cero.
El progreso se limpia automáticamente cuando la misión termina correctamente, de forma que la siguiente vez que se cargue aparezca como misión nueva. Solo persiste si la misión se interrumpió antes de completarse.
Gestión de batería: aviso, umbral y RTH automático
La gestión de batería se implementó como una coroutine que corre en paralelo a la navegación principal durante toda la misión, monitorizando el nivel reportado por DroneTelemetry. El umbral a partir del cual se activa el retorno automático es configurable mediante un deslizador en el panel de parámetros (entre el 10% y el 30%, con pasos de 5%), con 20% como valor por defecto.
El comportamiento tiene dos niveles. Cuando la batería cae a umbral+5% aparece un banner amarillo de aviso en la pantalla de ejecución: el operador sabe que le queda poco margen. Cuando alcanza el umbral exacto, el banner cambia a rojo y el dron inicia RTH automático. El progreso del waypoint actual ya está persistido en Room en ese momento, así que al aterrizar, cambiar la batería y reconectar, el botón «Reanudar WP N» estará esperando.
El flujo completo de una misión multi-batería queda así:
Vuelo en curso
│
├── Batería cae a umbral+5% → banner amarillo de aviso
│
├── Batería alcanza umbral → banner rojo + RTH automático
│ (progreso guardado en Room)
│
├── Dron aterriza
│
├── Operador: apagar dron → cambiar batería → encender → reconectar USB
│
└── Abrir app → cargar misión → botón "Reanudar WP N"
│
└── Despegar → volar directo al WP pendiente → continuar
La implementación tiene algunas protecciones necesarias: ignorar valores de batería de 0% (que indican dron desconectado, no batería agotada), garantizar que el RTH se dispara una sola vez aunque el nivel oscile alrededor del umbral, y no re-dispararlo si ya está en curso.
Revinculación de misiones antiguas con proyectos ODM
Un problema práctico surgió con las misiones procesadas antes de que existiera la persistencia del odmProjectId: esas misiones tienen resultados en WebODM pero la app no sabe a qué proyecto corresponden, así que no puede cargar el ortomosaico ni los hallazgos IA.
La solución fue un botón de revinculación que aparece en la lista de misiones cuando una misión tiene flight_id pero no odmProjectId: un icono naranja que al pulsarlo busca en WebODM un proyecto cuyo nombre contenga el prefijo del flight_id, extrae el ID de tarea y lo persiste. Un snackbar confirma el resultado o muestra el error si no se encuentra ningún proyecto coincidente.
Durante este desarrollo apareció otro bug de la API de WebODM: el endpoint GET /projects/ devuelve las tareas de cada proyecto como una lista de UUIDs en forma de strings, no como objetos TaskResponse completos. El modelo de datos de la app esperaba objetos y la deserialización fallaba silenciosamente. El cambio fue sencillo —List<TaskResponse> por List<String> en el modelo— pero ilustra que la API de WebODM tiene algunas inconsistencias que solo se descubren en la práctica.
Reorganización del selector de capas
Con la adición de las capas de ortomosaico y hallazgos IA, el selector de capas del mapa empezó a solaparse con los FABs de acción (Nueva misión, Cargar, Servidor). La solución fue doble: mover los chips de overlay (Ortomosaico ODM, Hallazgos IA) dentro del desplegable del selector de capas, junto a las capas base (OSM, PNOA, Catastro); y ocultar los FABs de acción mientras el selector está abierto. Tocar el mapa cierra el selector. El resultado es una interfaz que no se solapa y que agrupa de forma lógica todo lo relacionado con la visualización del mapa en un único control.
El estado del proyecto al cierre de la Fase 6
Con esta fase completada, el ciclo de uso de la aplicación es ya completo de extremo a extremo sin salir de la app: planificar la misión sobre el mapa, volarla con el dron de forma autónoma, reanudarla si la batería se acaba, ver los resultados del procesado ODM como overlay cartográfico, y consultar los hallazgos detectados por la IA durante el vuelo con sus coordenadas geográficas. Todo en la misma pantalla.
En el próximo artículo cubriré la Fase 7: la edición de polígono post-confirmación (mover, insertar y borrar vértices sin tener que redibujar el área), la corrección de la estimación de batería —que hasta ahora infraestimaba de forma notable el consumo real— y la caché offline de tiles de mapa para trabajar en campo sin cobertura de datos.