Dual fisheye, rig 360° y una lente que mira para el lado equivocado
Bitácora de investigación · Hyuga.ai · Artículo 2
En el primer artículo contamos la brecha entre las demos perfectas y lo que pasa cuando intentás digitalizar un espacio vos mismo. Esta semana corrimos el pipeline completo sobre tres datasets distintos, medimos cada etapa con cronómetro, y salieron varios aprendizajes que no esperábamos. El más raro: la pieza que arregló una de nuestras reconstrucciones fue la lente que apuntaba hacia donde no nos importaba mirar.
Antes de las fotos: elegir bien los frames
Todo el pipeline arranca con un video de la escena que después hay que convertir en imágenes para COLMAP. La forma más obvia es extraer un frame cada cierta cantidad de segundos. El problema es que si la persona que filma se mueve rápido en algún momento, dos frames consecutivos pueden estar muy lejos entre sí en el espacio, y COLMAP necesita overlap entre imágenes para encontrar puntos en común. Al mismo tiempo, si la cámara casi no se mueve en algún tramo, terminás con decenas de frames casi idénticos que no aportan información nueva pero sí suman tiempo de procesamiento.
Lo que hicimos fue dividir el video en segmentos temporales iguales y quedarnos con el mejor frame de cada segmento.
El proceso es así:
- Extraer todos los frames a fps alto (entre 20 y 60 según el video).
- Dividir esos frames en 200 segmentos temporales de igual duración.
- Por cada segmento, calcular un score de nitidez para cada frame (usamos la varianza del Laplaciano, una forma rápida de medir cuánto detalle tiene una imagen) y quedarse con el más nítido.
- Borrar todo lo demás.
- El resultado son 200 imágenes con cobertura temporal uniforme, sin saltos ni aglomeraciones, donde cada una es el frame más nítido de su ventana de tiempo.
La diferencia de nitidez entre datasets fue grande. El de azcuenaga, filmado con buena luz y movimiento lento, tuvo un score promedio de 518. El de fuente-360, con más movimiento y peor luz, llegó a 124. Esa diferencia se traduce en cuántos features detecta COLMAP por imagen y, por ende, en la densidad de la nube de puntos.
Segmentar y elegir por nitidez es más trabajo de setup que extraer a intervalo fijo, pero garantiza que cada posición del recorrido esté representada por el mejor frame disponible.
El rig de dos cámaras: cómo funciona realmente
Todo lo que sigue en este artículo asume una cámara 360° con dos lentes fisheye. El proceso funciona también con cámaras convencionales, fisheye individuales, o setups de múltiples cámaras. En el primer artículo explicamos por qué elegimos la 360°, básicamente el mejor balance entre costo de hardware y tolerancia al error de captura. Acá nos enfocamos en ese formato porque es donde concentramos la mayor parte de nuestra investigación. Si trabajás con otra configuración, algunos de estos aprendizajes aplican igual y otros no.
Una cámara 360° tiene dos lentes fisheye, una mirando hacia adelante y otra hacia atrás, separadas 180°. Cada una captura un hemisferio. El video que graba la cámara es una imagen equirectangular: la proyección del mundo esférico aplanada en un rectángulo, como un mapamundi.
Para alimentar a COLMAP hay que hacer el proceso inverso: tomar esa imagen equirectangular y generar dos imágenes fisheye separadas, una por hemisferio, que correspondan a lo que cada lente capturaría de forma nativa.
Eso suena directo, pero fue el primer lugar donde cometimos un error que nos costó tiempo.
El split que no funcionaba
El primer script que escribimos usaba una función de OpenCV (fisheye.undistortPoints) con un shift horizontal para separar las dos cámaras. Producía dos imágenes, pero la geometría era incorrecta, no correspondía a cómo COLMAP modela un rig. Las imágenes se veían razonables y la reconstrucción igualmente fallaba o era inestable.
Terminamos tirando ese script y reescribiéndolo siguiendo el flujo oficial de COLMAP (panorama_sfm.py). El cambio fue trabajar con rayos en lugar de con píxeles: para cada píxel de la imagen equirectangular se calcula la dirección del rayo en el espacio 3D, ese rayo se rota (cam0 a yaw 0°, cam1 a yaw 180°) y se proyecta al modelo fisheye correspondiente. También se genera una máscara que asigna cada píxel a exactamente una cámara, sin superposiciones.
Ese proceso produce imágenes que COLMAP puede usar con la geometría correcta, porque la relación entre las dos cámaras es matemáticamente coherente con cómo va a modelar el rig.

El ángulo importa: 64° vs 89°
Con el split bien hecho, apareció el segundo problema: cuánto ángulo cubrir.
El ejemplo de COLMAP para fisheye usa focal = height × 0.45, que a resolución 3840px da una focal de 1728px. Cada cámara captura un cono de ~64° desde el eje óptico. En 180° por hemisferio, eso deja un hueco de ~26° sin cobertura alrededor de la costura entre las dos cámaras, en los costados (yaw ±90°).

height × 0.45), cada cámara cubre ~64° y deja un hueco de ~26° en la costura entre hemisferios.En la práctica el hueco es visible: zonas sin puntos, matches débiles justo donde las dos cámaras deberían conectarse.
Calculamos la focal exacta para que el borde del círculo fisheye llegue a 90° desde el eje óptico, el punto donde un hemisferio termina y el otro empieza. Le llamamos full-seam. La focal resultante es 1234.3px (ratio 0.321 sobre la altura), con un rim medido de 89.1°. Los puntos totales entre ambas configuraciones son parecidos, pero el error de reproyección baja y la costura se cierra. En un espacio donde se va a explorar libremente, esa zona sin cobertura hubiera sido visible en el resultado final.
Usar el ejemplo default de COLMAP deja zonas sin cobertura en la costura. Hay que calcular la focal para que los dos hemisferios se encuentren.
El rig vs cámara simple: lo que encontramos en este caso
Esta semana también probamos con una cámara simple en perspectiva, sobre un dataset de una fuente en una plaza. Vale aclarar que era un caso donde la cámara simple tenía sentido: la fuente estaba completamente visible en el encuadre de un solo lente, sin necesitar los 360°. Era una prueba legítima donde una cámara convencional podría haber funcionado. En nuestro caso, no fue así.
El rig fisheye registró 400 imágenes a la primera, sin problemas de calibración. La cámara simple necesitó cuatro intentos para producir algo usable, en buena parte por errores de calibración que en el rig no aparecen porque la focal se calcula de la geometría del split.
| Métrica | Rig fisheye | Cámara simple |
|---|---|---|
| Imágenes registradas | 400/400 | 200/200 |
| Intentos necesarios | 1 | 4 |
| Puntos 3D | 225k–319k | 76k–89k |
| Largo de track promedio | 16.8 | ~5–8 |
| Problemas de calibración | Ninguno | Focal mal escalada, modelo incorrecto |
Hay algunas razones estructurales que explican por qué el rig se comportó mejor en este experimento.
Con el rig, la focal surge de la geometría del split: es un número exacto que COLMAP recibe sin tener que estimarlo. Con la cámara simple y sin datos EXIF confiables, COLMAP necesita un prior externo. Si ese prior está mal escalado, como en nuestro caso donde la calibración se había hecho a un cuarto de la resolución real, el proceso falla.
El rig también le da a COLMAP una restricción que la cámara sola no tiene: la posición relativa entre las dos lentes es fija y conocida (una a 0°, otra a 180°). El mapper no tiene que resolver esa relación desde los feature matches; ya la conoce. Eso reduce los grados de libertad del problema.
Y está la lente de atrás. Si lo que querés mostrar está adelante, esa lente captura pared, techo, cosas que no van a aparecer en el resultado final. Pero COLMAP reconstruye el espacio completo y la posición de la cámara dentro de él. Más puntos distintivos alrededor, en cualquier dirección, le dan al algoritmo más anclas para resolver dónde estaba la cámara en cada disparo. Las dos lentes juntas cubren toda la esfera, así que cada punto de la escena se ve desde muchos ángulos. El largo de track promedio en el rig fue de 16.8 imágenes por punto; en la cámara simple, entre 5 y 8. Para escenas donde una cámara simple tiene buena calibración y buena cobertura, los resultados pueden acercarse. En nuestro caso no fue así.
COLMAP: modos de reconstrucción y resultados de nuestras pruebas
COLMAP tiene varias variables independientes que afectan el resultado: el mapper puede ser incremental o global (GLOMAP), cada etapa puede correr en CPU o CUDA, y SIFT tiene versión CPU y GPU. Cambiar cualquiera de esas opciones altera tiempo, densidad de la nube y estabilidad.
Lo que sigue son resultados de nuestras pruebas sobre el dataset azcuenaga (rig fisheye, full-seam), con distintas combinaciones.
| Prueba | Hardware | Mapper | Tiempo | Puntos 3D |
|---|---|---|---|---|
| Incremental, Mac local | Apple M4 Pro, CPU | incremental | 26.5 min | 319.359 |
| Incremental, pod | NVIDIA A6000, CUDA | incremental | ~24 min | 261.378 |
| Global, pod | NVIDIA A6000, CUDA | global (GLOMAP) | 15 min | 226.018 |
Con mapper incremental, Mac CPU y pod CUDA tardaron parecido (24-26 min) pero la Mac generó más puntos. El global en pod tardó la mitad pero produjo ~30% menos puntos que el incremental en Mac.
Un dato que no esperábamos: en nuestras pruebas, SIFT en GPU detectó aproximadamente un 22% menos de puntos por imagen que en CPU, lo que se tradujo en ~18% menos puntos 3D al final. Si corrés en pod y la nube te queda menos densa de lo esperado, probablemente sea eso.
Para iterar rápido sobre el pipeline, el global en GPU es más práctico. Para la nube más densa, el incremental en Mac CPU dio el mejor resultado en balance tiempo/calidad.
Configuración que usamos
-
Split dual fisheye full-seam. Es la configuración que cerró la costura entre hemisferios y dio los mejores resultados. El preset 0.45 de COLMAP deja un hueco en yaw ±90° que se ve en el resultado final.
-
Preprocesar el input antes de correr COLMAP. A partir de los 200 panos equirectangulares (relación 2:1), generar 400 imágenes fisheye, 2 por pano, organizadas en dos carpetas:
pano_camera0/(yaw 0°) ypano_camera1/(yaw 180°). -
Focal calculada para rim ≈ 90°, modelo OPENCV_FISHEYE, camera_mode PER_FOLDER. No usar el preset 0.45. El
camera_mode PER_FOLDERle dice a COLMAP que hay un modelo de cámara por carpeta con intrínsecos iguales dentro de cada una, que es exactamente la estructura del rig. -
Máscaras completas para ambas cámaras.
masks/pano_camera0/ymasks/pano_camera1/, con namingseg_001.jpg → seg_001.jpg.png. Sin máscaras, los píxeles compartidos entre hemisferios rompen el rig.
Lo que sigue
Las preguntas que nos quedan abiertas:
- ¿Cuánto impacta en el splat final la densidad de la nube sparse de COLMAP, versus otros parámetros del entrenamiento?
- ¿Qué partes del pipeline full-seam (split, máscaras, etc) se pueden automatizar sin reintroducir errores de geometría?
- ¿En qué tipos de escena el rig dual fisheye sigue ganándole a una cámara simple bien calibrada, y en cuáles deja de importar?
Hyuga.ai — investigación en curso ¿Estás trabajando con cámaras 360° o convencionales? Nos interesa comparar notas.