tutoriales.com

Alineación y Calibración de Cámaras para Visión Estereoscópica 👁️‍🗨️

Este tutorial profundiza en los fundamentos y la práctica de la alineación y calibración de sistemas de cámaras estéreo. Exploraremos los conceptos clave, el proceso detallado utilizando OpenCV y cómo aplicar estos conocimientos para obtener mediciones 3D precisas.

Intermedio15 min de lectura7 views
Reportar error

La visión estereoscópica es una técnica fundamental en la visión artificial que permite a las máquinas percibir la profundidad, de manera similar a cómo lo hacen los humanos. Para que un sistema estéreo funcione correctamente, es crucial que sus cámaras estén correctamente alineadas y calibradas. Sin una calibración precisa, las mediciones de profundidad serán inexactas, comprometiendo la fiabilidad de cualquier aplicación que dependa de ellas.

En este tutorial, desglosaremos los conceptos detrás de la visión estéreo, la importancia de la calibración y el proceso práctico para lograrla utilizando la biblioteca OpenCV.

🧐 ¿Qué es la Visión Estereoscópica?

La visión estereoscópica es el proceso de obtener información de profundidad a partir de dos o más imágenes tomadas desde diferentes puntos de vista. Al analizar las pequeñas diferencias, o disparidades, entre las imágenes, podemos inferir la distancia de los objetos a la cámara.

💡 El Principio de la Disparidad

Imagina que miras un objeto con ambos ojos. Si cierras un ojo y luego el otro, notarás que el objeto parece "moverse" ligeramente contra el fondo. Este "movimiento" es la disparidad. Cuanto más cerca está el objeto, mayor es el movimiento aparente (disparidad). La visión estereoscópica utiliza este mismo principio.

En un sistema de dos cámaras, cada cámara captura una imagen. Para un mismo punto en el mundo real, su proyección en cada imagen será ligeramente diferente. La diferencia en las coordenadas de pixel de este punto en las dos imágenes se conoce como disparidad. Esta disparidad está inversamente relacionada con la profundidad del punto.

P (Punto en el espacio) Plano Imagen Izq. Plano Imagen Der. P_izq P_der Cámara Izq. Cámara Der. Línea Base (B) Disparidad (d) = x_izq - x_der Diferencia de posición en los sensores

🎯 Componentes Clave de un Sistema Estéreo

Un sistema estéreo básico consta de:

  • Dos cámaras: Capturan imágenes desde diferentes perspectivas.
  • Línea base: La distancia y orientación entre los centros ópticos de las dos cámaras. Es crucial para el cálculo de profundidad.
  • Algoritmo de correspondencia: Encuentra el mismo punto en ambas imágenes (el paso más desafiante).
  • Algoritmo de triangulación: Utiliza la disparidad y los parámetros de la cámara para calcular la profundidad 3D.

🔥 ¿Por qué es Crucial la Calibración de Cámaras?

Antes de poder calcular la profundidad con precisión, necesitamos conocer las características internas de cada cámara (calibración intrínseca) y la relación espacial entre ellas (calibración extrínseca).

🛠️ Calibración Intrínseca

La calibración intrínseca determina los parámetros internos de una cámara que distorsionan la imagen y cómo se proyectan los puntos 3D en el plano 2D de la imagen. Estos parámetros incluyen:

  • Matriz de la cámara (K): Contiene la longitud focal (fx, fy) y el punto principal (cx, cy).
  • Coeficientes de distorsión (D): Modelan las distorsiones radiales y tangenciales causadas por la lente.
📌 Nota: La distorsión de la lente hace que las líneas rectas en el mundo real aparezcan curvadas en la imagen. Corregir esto es fundamental para mediciones precisas.

↔️ Calibración Extrínseca (Alineación)

La calibración extrínseca, a menudo llamada alineación estéreo, determina la relación de rotación y traslación entre las dos cámaras. Nos dice cómo una cámara está posicionada y orientada en relación con la otra. Los parámetros clave son:

  • Matriz de rotación (R): Describe la rotación de la segunda cámara con respecto a la primera.
  • Vector de traslación (T): Describe la traslación de la segunda cámara con respecto a la primera.
🔥 Importante: Sin estos parámetros extrínsecos, no podemos establecer una correspondencia correcta entre los puntos de las dos imágenes ni triangular sus posiciones 3D.

📝 El Proceso de Calibración Estéreo Paso a Paso

El proceso de calibración estéreo generalmente sigue los siguientes pasos:

Paso 1: Captura de imágenes del patrón de calibración.
Paso 2: Detección de puntos clave en el patrón.
Paso 3: Calibración intrínseca de cada cámara individualmente.
Paso 4: Calibración estéreo para encontrar R y T entre las cámaras.
Paso 5: Rectificación de las imágenes para simplificar la correspondencia.
Paso 6: Validación de la calibración.

Usaremos Python y OpenCV para ilustrar cada paso.

🧩 Materiales Necesarios

  • Dos cámaras: USB o IP, preferiblemente con la misma resolución.
  • Patrón de calibración: Un tablero de ajedrez (chessboard) impreso con precisión es el más común. Debe tener un tamaño de cuadrados conocido.
  • Python 3.x
  • OpenCV: pip install opencv-python
  • NumPy: pip install numpy

🔳 Preparando el Patrón de Calibración

Un patrón de tablero de ajedrez es ideal porque sus esquinas son fáciles de detectar con alta precisión. Necesitas saber:

  • El número de esquinas interiores a lo largo de las filas (chessboardSizeX).
  • El número de esquinas interiores a lo largo de las columnas (chessboardSizeY).
  • El tamaño real de un cuadrado en el patrón (ej. squareSize = 25.0 mm).

Ejemplo: Un tablero de 8x6 cuadrados tiene 7x5 esquinas interiores.

📸 Paso 1 y 2: Captura y Detección de Esquinas

El primer paso es capturar múltiples pares de imágenes del patrón de calibración desde diferentes orientaciones y distancias, asegurándose de que el patrón sea visible en ambas cámaras simultáneamente y cubra diferentes partes del campo de visión. Luego, detectaremos las esquinas del tablero de ajedrez.

import numpy as np
import cv2
import glob

# Dimensiones del tablero de ajedrez (esquinas interiores)
chessboardSize = (7, 5) # 7 en X, 5 en Y para un tablero de 8x6 cuadrados
# Tamaño del cuadrado en el mundo real (mm)
squareSize = 25.0

# Criterios para la detección de esquinas sub-pixel
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)

# Prepara los puntos del objeto (0,0,0), (1,0,0), (2,0,0) ....,(6,5,0)
objp = np.zeros((chessboardSize[0] * chessboardSize[1], 3), np.float32)
objp[:,:2] = np.mgrid[0:chessboardSize[0], 0:chessboardSize[1]].T.reshape(-1, 2)
objp = objp * squareSize

# Arrays para almacenar los puntos del objeto y los puntos de la imagen de todas las imágenes.
objpoints = [] # puntos 3D en el espacio del mundo real
imgpointsL = [] # puntos 2D en el plano de la imagen izquierda
imgpointsR = [] # puntos 2D en el plano de la imagen derecha

# Ruta a las imágenes capturadas (asegúrate de que estén emparejadas)
imagesLeft = sorted(glob.glob('images_left/*.jpg'))
imagesRight = sorted(glob.glob('images_right/*.jpg'))

for imgLeftPath, imgRightPath in zip(imagesLeft, imagesRight):
    imgL = cv2.imread(imgLeftPath)
    imgR = cv2.imread(imgRightPath)
    grayL = cv2.cvtColor(imgL, cv2.COLOR_BGR2GRAY)
    grayR = cv2.cvtColor(imgR, cv2.COLOR_BGR2GRAY)

    # Encontrar las esquinas del tablero de ajedrez
    retL, cornersL = cv2.findChessboardCorners(grayL, chessboardSize, None)
    retR, cornersR = cv2.findChessboardCorners(grayR, chessboardSize, None)

    if retL == True and retR == True:
        objpoints.append(objp)
        # Refinar la posición de las esquinas para mayor precisión
        cornersL = cv2.cornerSubPix(grayL, cornersL, (11,11), (-1,-1), criteria)
        cornersR = cv2.cornerSubPix(grayR, cornersR, (11,11), (-1,-1), criteria)
        imgpointsL.append(cornersL)
        imgpointsR.append(cornersR)

        # Opcional: dibujar las esquinas para verificar
        # cv2.drawChessboardCorners(imgL, chessboardSize, cornersL, retL)
        # cv2.drawChessboardCorners(imgR, chessboardSize, cornersR, retR)
        # cv2.imshow('img left', imgL)
        # cv2.imshow('img right', imgR)
        # cv2.waitKey(500)

cv2.destroyAllWindows()
print("Número de pares de imágenes procesados: ", len(objpoints))

📐 Paso 3: Calibración Intrínseca de Cámaras Individuales

Ahora, calibraremos cada cámara de forma independiente para obtener sus parámetros intrínsecos y de distorsión. Esto se hace con la función cv2.calibrateCamera.

# Calibración de la cámara izquierda
retL, cameraMatrixL, distCoeffsL, rvecsL, tvecsL = cv2.calibrateCamera(objpoints,
                                                                     imgpointsL, grayL.shape[::-1], None, None)
# Calibración de la cámara derecha
retR, cameraMatrixR, distCoeffsR, rvecsR, tvecsR = cv2.calibrateCamera(objpoints,
                                                                     imgpointsR, grayR.shape[::-1], None, None)

print("\n--- Calibración Intrínseca Cámara Izquierda ---")
print("Matriz de la cámara izquierda:\n", cameraMatrixL)
print("Coeficientes de distorsión izquierda:\n", distCoeffsL)

print("\n--- Calibración Intrínseca Cámara Derecha ---")
print("Matriz de la cámara derecha:\n", cameraMatrixR)
print("Coeficientes de distorsión derecha:\n", distCoeffsR)

🤝 Paso 4: Calibración Estéreo

Con las calibraciones intrínsecas listas, procedemos a la calibración estéreo. Esta función (cv2.stereoCalibrate) nos dará la matriz de rotación R y el vector de traslación T entre las dos cámaras, así como sus matrices de proyección rectificadas P1 y P2 y las matrices de rectificación R1 y R2.

flags = 0
flags |= cv2.CALIB_FIX_INTRINSIC # Fijamos los intrínsecos calculados previamente
# flags |= cv2.CALIB_USE_INTRINSIC_GUESS # Opcional: si queremos usar intrínsecos como punto de partida
# flags |= cv2.CALIB_FIX_PRINCIPAL_POINT
# flags |= cv2.CALIB_RATIONAL_MODEL # Para modelos de distorsión más complejos
# flags |= cv2.CALIB_THIN_PRISM_MODEL # Otro modelo de distorsión
# flags |= cv2.CALIB_FIX_K1 # ... CALIB_FIX_K6
# flags |= cv2.CALIB_ZERO_TANGENT_DIST # Asume distorsión tangencial cero

retS, newCameraMatrixL, distCoeffsL, newCameraMatrixR, distCoeffsR,\
    R, T, E, F = cv2.stereoCalibrate(objpoints, imgpointsL, imgpointsR,
                                      cameraMatrixL, distCoeffsL, cameraMatrixR, distCoeffsR,
                                      grayL.shape[::-1], criteria, flags)

print("\n--- Calibración Estéreo ---")
print("Matriz de Rotación (R):\n", R)
print("Vector de Traslación (T):\n", T)
print("Error de reproyección estéreo: ", retS)
⚠️ Advertencia: Un error de reproyección estéreo alto (ej. > 1.0 pixels) puede indicar problemas en la captura de imágenes, la detección de esquinas o la calidad del patrón.

💫 Paso 5: Rectificación Estéreo

La rectificación estéreo es un paso crucial que transforma las imágenes de cada cámara de tal manera que todos los puntos correspondientes se encuentran en la misma línea horizontal (epipolar) en ambas imágenes. Esto simplifica enormemente el problema de correspondencia al reducir la búsqueda a una dimensión.

cv2.stereoRectify calcula las matrices de rotación R1, R2 y de proyección P1, P2 para las cámaras rectificadas, además de una matriz Q para la reproyección 3D.

R1, R2, P1, P2, Q, roi_L, roi_R = cv2.stereoRectify(newCameraMatrixL, distCoeffsL,
                                                      newCameraMatrixR, distCoeffsR,
                                                      grayL.shape[::-1], R, T, alpha=1)

# Mapeos para la undistorsión y rectificación
mapL1, mapL2 = cv2.initUndistortRectifyMap(newCameraMatrixL, distCoeffsL, R1, P1, grayL.shape[::-1], cv2.CV_16SC2)
mapR1, mapR2 = cv2.initUndistortRectifyMap(newCameraMatrixR, distCoeffsR, R2, P2, grayR.shape[::-1], cv2.CV_16SC2)

print("\n--- Parámetros de Rectificación ---")
print("Matriz de Proyección Izquierda (P1):\n", P1)
print("Matriz de Proyección Derecha (P2):\n", P2)
print("Matriz de reproyección Q:\n", Q)
💡 Consejo: El parámetro `alpha` en `stereoRectify` controla el zoom de la imagen rectificada. `alpha=0` recorta la imagen para eliminar píxeles inválidos, `alpha=1` mantiene todos los píxeles originales pero puede introducir bordes negros.

✅ Paso 6: Verificación de la Rectificación

Para verificar que la rectificación se realizó correctamente, podemos tomar un par de imágenes estéreo, rectificarlas y dibujar líneas horizontales. Los puntos correspondientes deben caer sobre la misma línea.

# Cargar un par de imágenes para demostración
imgL = cv2.imread(imagesLeft[0]) # Usamos el primer par de la lista
imgR = cv2.imread(imagesRight[0])

# Rectificar las imágenes
undistortedL = cv2.remap(imgL, mapL1, mapL2, cv2.INTER_LINEAR)
undistortedR = cv2.remap(imgR, mapR1, mapR2, cv2.INTER_LINEAR)

# Juntar las imágenes rectificadas para visualización
output = np.hstack((undistortedL, undistortedR))

# Dibujar líneas horizontales para verificar la alineación
for y in range(0, output.shape[0], 20):
    cv2.line(output, (0, y), (output.shape[1], y), (0, 255, 0), 1)

cv2.imshow('Rectified Images with Epipolar Lines', output)
cv2.waitKey(0)
cv2.destroyAllWindows()

Si la rectificación es exitosa, las líneas de la cuadrícula en las imágenes originales (si el patrón es una cuadrícula) deben aparecer perfectamente horizontales y alineadas entre ambas imágenes rectificadas.

💾 Guardar los Parámetros de Calibración

Una vez calibrado el sistema, es fundamental guardar todos los parámetros para poder cargarlos y utilizarlos más tarde sin necesidad de recalibrar. Esto incluye cameraMatrixL, distCoeffsL, cameraMatrixR, distCoeffsR, R, T, R1, R2, P1, P2, Q, mapL1, mapL2, mapR1, mapR2.

import pickle

calibration_params = {
    "cameraMatrixL": newCameraMatrixL,
    "distCoeffsL": distCoeffsL,
    "cameraMatrixR": newCameraMatrixR,
    "distCoeffsR": distCoeffsR,
    "R": R,
    "T": T,
    "R1": R1,
    "R2": R2,
    "P1": P1,
    "P2": P2,
    "Q": Q,
    "mapL1": mapL1,
    "mapL2": mapL2,
    "mapR1": mapR1,
    "mapR2": mapR2
}

with open('stereo_calibration_params.pkl', 'wb') as f:
    pickle.dump(calibration_params, f)

print("Parámetros de calibración guardados en stereo_calibration_params.pkl")

📏 Cálculo de Profundidad 3D (Reconstrucción)

Una vez que las cámaras están calibradas y las imágenes rectificadas, el siguiente paso es calcular el mapa de disparidades y luego reproyectarlo a 3D. El mapa de disparidades asigna a cada píxel la diferencia en su posición x entre la imagen izquierda y derecha rectificada.

🖥️ Computando el Mapa de Disparidades

OpenCV proporciona algoritmos como StereoBM (Block Matching) o StereoSGBM (Semi-Global Block Matching) para calcular el mapa de disparidades.

# Cargar las imágenes rectificadas (usando las mismas de la verificación)
imgL_rect = undistortedL
imgR_rect = undistortedR

# Convertir a escala de grises para los algoritmos de disparidad
grayL_rect = cv2.cvtColor(imgL_rect, cv2.COLOR_BGR2GRAY)
grayR_rect = cv2.cvtColor(imgR_rect, cv2.COLOR_BGR2GRAY)

# Configurar el algoritmo StereoBM (para mejor rendimiento, usar SGBM)
stereo = cv2.StereoBM_create(numDisparities=16*5, blockSize=21) # numDisparities debe ser divisible por 16
# Los parámetros numDisparities y blockSize son cruciales para la calidad del mapa de disparidades

disparity = stereo.compute(grayL_rect, grayR_rect)

# Normalizar y mostrar el mapa de disparidades
disparity_normalized = cv2.normalize(disparity, None, 255, 0, cv2.NORM_MINMAX).astype(np.uint8)
cv2.imshow('Disparity Map', disparity_normalized)
cv2.waitKey(0)
cv2.destroyAllWindows()

🌐 Reproyección a 3D

Con el mapa de disparidades y la matriz Q obtenida de stereoRectify, podemos transformar cada píxel (x, y, disparidad) en coordenadas 3D (X, Y, Z) en el espacio real. Esto se hace con cv2.reprojectImageTo3D.

# Cargar la matriz Q si no está en memoria (ej. después de cargar de un archivo)
# with open('stereo_calibration_params.pkl', 'rb') as f:
#     cal_params = pickle.load(f)
# Q = cal_params['Q']

points_3D = cv2.reprojectImageTo3D(disparity, Q)

# El array points_3D contendrá las coordenadas (X, Y, Z) para cada píxel de la imagen izquierda.
# Podemos acceder a la profundidad (Z) de un punto específico, por ejemplo, el centro de la imagen:

center_x, center_y = grayL_rect.shape[1] // 2, grayL_rect.shape[0] // 2

# Asegurarse de que el píxel tiene una disparidad válida (no -1 o valores muy bajos)
if disparity[center_y, center_x] > 0:
    depth_at_center = points_3D[center_y, center_x, 2] # El tercer componente es Z (profundidad)
    print(f"Profundidad en el centro de la imagen: {depth_at_center:.2f} mm")
else:
    print("No se pudo calcular la profundidad en el centro (disparidad inválida).")

# Para visualizar la nube de puntos 3D, se pueden usar librerías como Open3D o Matplotlib 3D
# 
Proyección de Nube de Puntos 3D Z (Altura) X (Largo) Y (Ancho) Leyenda: Plano Superior Plano Lateral X Plano Lateral Y Escena proyectada: Geometría de caja básica en coordenadas espaciales.

## 📈 Aplicaciones Comunes de la Visión Estéreo Calibrada

La visión estéreo correctamente calibrada es la base de muchas aplicaciones de visión artificial avanzadas:

*   **Robótica:** Navegación autónoma, evitación de obstáculos, manipulación de objetos.
*   **Realidad Virtual/Aumentada:** Reconstrucción de escenas, seguimiento de objetos y usuarios.
*   **Inspección Industrial:** Medición de dimensiones, detección de defectos en 3D.
*   **Automoción:** Detección de peatones y vehículos, estimación de distancia, mapeo 3D.
*   **Medicina:** Cirugía asistida por robot, análisis de imágenes médicas 3D.

<div class="progress-bar"><div class="progress-fill" style="width: 90%; background: #28A745;">90% Precisión 3D</div></div>

## 🤔 Preguntas Frecuentes (FAQ)

<details open><summary>¿Cuántas imágenes necesito para una buena calibración?</summary>Generalmente, se recomiendan entre 10 y 20 pares de imágenes para una calibración robusta. Es importante que estas imágenes capturen el patrón desde diferentes ángulos, distancias y orientaciones, cubriendo todo el campo de visión de ambas cámaras.</details>

<details open><summary>¿Qué tamaño debe tener el patrón de calibración?</summary>El patrón debe ser lo suficientemente grande para ser visible en ambas cámaras y permitir la detección de un buen número de esquinas. El tamaño de los cuadrados debe ser conocido con precisión y el patrón debe estar impreso en un material plano y rígido para evitar deformaciones.</details>

<details open><summary>¿Es `StereoBM` o `StereoSGBM` mejor?</summary><code>StereoSGBM</code> (Semi-Global Block Matching) generalmente ofrece mejores resultados en términos de calidad del mapa de disparidades, ya que considera la continuidad de la disparidad entre píxeles, resultando en mapas más suaves y menos ruidosos. Sin embargo, es computacionalmente más intensivo que <code>StereoBM</code>.</details>

<details open><summary>¿Qué significa un error de reproyección bajo?</summary>Un error de reproyección bajo (típicamente < 1.0 píxeles) indica que los parámetros de la cámara calculados por el algoritmo de calibración son consistentes con la forma en que los puntos 3D del patrón se proyectan en las imágenes 2D. Un valor más bajo significa una calibración más precisa.</details>

## Conclusion

La alineación y calibración de cámaras es un pilar fundamental para cualquier sistema de visión estereoscópica que busque obtener mediciones 3D precisas y fiables. Hemos explorado los conceptos clave, el proceso paso a paso con OpenCV y cómo aplicar estos conocimientos para la reconstrucción 3D.

Al dominar estas técnicas, estarás bien equipado para desarrollar aplicaciones de visión artificial que requieren una comprensión profunda del espacio tridimensional.

Tutoriales relacionados

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!