← Volver al blog

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í:

  1. Extraer todos los frames a fps alto (entre 20 y 60 según el video).
  2. Dividir esos frames en 200 segmentos temporales de igual duración.
  3. 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.
  4. Borrar todo lo demás.
  5. 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.
Imagen equirectangular 360° dividida en dos hemisferios fisheye para COLMAP
De equirectangular a dos imágenes fisheye, una por hemisferio, listas para alimentar el rig de COLMAP.

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.

Split fisheye con geometría correcta pero cobertura incompleta en algunas zonas
Split resuelto con geometría correcta (flujo de rayos de COLMAP), pero que todavía no cubría ciertas zonas del hemisferio.

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°).

Cono de cobertura fisheye con focal default de COLMAP, ~64° por cámara
Con la focal default de COLMAP (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.
Comparación del dataset azcuenaga: reconstrucción con focal default vs full-seam
COLMAP default (~64°) Full-seam (89°)
Dataset azcuenaga: arrastrá el control para comparar la costura con focal default de COLMAP vs full-seam (1234px, rim 89°).

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étricaRig fisheyeCámara simple
Imágenes registradas400/400200/200
Intentos necesarios14
Puntos 3D225k–319k76k–89k
Largo de track promedio16.8~5–8
Problemas de calibraciónNingunoFocal 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.

PruebaHardwareMapperTiempoPuntos 3D
Incremental, Mac localApple M4 Pro, CPUincremental26.5 min319.359
Incremental, podNVIDIA A6000, CUDAincremental~24 min261.378
Global, podNVIDIA A6000, CUDAglobal (GLOMAP)15 min226.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°) y pano_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_FOLDER le 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/ y masks/pano_camera1/, con naming seg_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.

0 me gusta

Comentarios