Para implementar un sistema de visión sincronizado con múltiples sensores utilizando hardware de Luxonis, se requiere una integración precisa entre el firmware de la cámara y el software de procesamiento. En este artículo se detalla cómo configurar un arreglo de cuatro cámaras OV9782 conectadas a una placa OAK-FFC-4P, permitiendo el control dinámico de la exposición y el balance de blancos, además de la integración con ROS (Robot Operating System).
Requisitos de Hardware
- 4 sensores de imagen OV9782.
- Placa base OAK-FFC-4P.
- Cableado para sincronización de disparo por hardware (Hardware Trigger).
Configuración del Entorno
En sistemas basados en Linux, como Ubuntu, es fundamental preparar el entorno de Python y las reglas de acceso al bus USB para los dispositivos Movidius.
# Obtención del SDK de DepthAI
git clone https://github.com/luxonis/depthai.git
cd depthai
# Instalación de dependencias necesarias
python3 -m pip install -r requirements.txt
# Configuración de reglas udev para el dispositivo
echo 'SUBSYSTEM=="usb", ATTRS{idVendor}=="03e7", MODE="0666"' | sudo tee /etc/udev/rules.d/80-movidius.rules
sudo udevadm control --reload-rules && sudo udevadm trigger
Implementación del Control de Cámara y Publicación en ROS
El siguiente script configura el pipeline de DepthAI para gestionar cuatro flujos de video sincronizados. Incluye una lógica para alternar entre el modo de exposición automática y manual, enviando los comandos a través de una cola de entrada de control.
# -*- coding: utf-8 -*-
import depthai as dai
import cv2
import numpy as np
import rospy
from sensor_msgs.msg import Image
from std_msgs.msg import Header
class GestorCamarasOAK:
def __init__(self, frecuencia=20, auto_modo=True):
self.fps = frecuencia
self.modo_automatico = auto_modo
self.nombres_nodos = ['A', 'B', 'C', 'D']
self.instancia_pipeline = dai.Pipeline()
self.unidades_camara = {}
self.salidas_xlink = {}
# Mapeo de sockets físicos
self.mapeo_sockets = {
'A': dai.CameraBoardSocket.CAM_A,
'B': dai.CameraBoardSocket.CAM_B,
'C': dai.CameraBoardSocket.CAM_C,
'D': dai.CameraBoardSocket.CAM_D
}
# Configuración del flujo de control
self.entrada_control = self.instancia_pipeline.create(dai.node.XLinkIn)
self.entrada_control.setStreamName('control_input')
for id_cam in self.nombres_nodos:
cam_node = self.instancia_pipeline.createColorCamera()
cam_node.setResolution(dai.ColorCameraProperties.SensorResolution.THE_800_P)
cam_node.setBoardSocket(self.mapeo_sockets[id_cam])
cam_node.setFps(self.fps)
# Sincronización por hardware: La cámara A actúa como salida de trigger
if id_cam == 'A':
cam_node.initialControl.setFrameSyncMode(dai.CameraControl.FrameSyncMode.OUTPUT)
else:
cam_node.initialControl.setFrameSyncMode(dai.CameraControl.FrameSyncMode.INPUT)
self.unidades_camara[id_cam] = cam_node
# Configuración de salida
xout = self.instancia_pipeline.createXLinkOut()
xout.setStreamName(f'raw_{id_cam}')
cam_node.isp.link(xout.input)
self.salidas_xlink[id_cam] = xout
# Configuración de GPIO para habilitar disparo externo si es necesario
config_dispositivo = dai.Device.Config()
config_dispositivo.board.gpio[6] = dai.BoardConfig.GPIO(dai.BoardConfig.GPIO.OUTPUT, dai.BoardConfig.GPIO.Level.HIGH)
self.dispositivo = dai.Device(config_dispositivo)
def iniciar(self):
self.dispositivo.startPipeline(self.instancia_pipeline)
self.colas_captura = {
nombre: self.dispositivo.getOutputQueue(name=f'raw_{nombre}', maxSize=1, blocking=False)
for nombre in self.nombres_nodos
}
self.cola_ctrl = self.dispositivo.getInputQueue('control_input')
self._aplicar_ajustes_iniciales()
def _aplicar_ajustes_iniciales(self):
ctrl = dai.CameraControl()
if self.modo_automatico:
ctrl.setAutoExposureEnable()
ctrl.setAutoWhiteBalanceMode(dai.CameraControl.AutoWhiteBalanceMode.AUTO)
else:
# Valores manuales predefinidos
t_exposicion = 12000 # microsegundos
iso_valor = 400
ctrl.setManualExposure(t_exposicion, iso_valor)
ctrl.setManualWhiteBalance(4000)
self.cola_ctrl.send(ctrl)
def obtener_frames(self):
frames = {}
stamps = {}
for id_cam, cola in self.colas_captura.items():
paquete = cola.tryGet()
if paquete is not None:
frames[id_cam] = paquete.getCvFrame()
stamps[id_cam] = paquete.getTimestampDevice().total_seconds()
else:
frames[id_cam] = None
return frames, stamps
def publicar_en_ros():
rospy.init_node('nodo_oak_multiple', anonymous=True)
publicadores = {
'A': rospy.Publisher('/oak/cam_a/image_raw', Image, queue_size=1),
'B': rospy.Publisher('/oak/cam_b/image_raw', Image, queue_size=1),
'C': rospy.Publisher('/oak/cam_c/image_raw', Image, queue_size=1),
'D': rospy.Publisher('/oak/cam_d/image_raw', Image, queue_size=1)
}
obj_camara = GestorCamarasOAK(frecuencia=20, auto_modo=True)
obj_camara.iniciar()
rate = rospy.Rate(20)
while not rospy.is_shutdown():
dic_img, dic_ts = obj_camara.obtener_frames()
for id_cam, frame in dic_img.items():
if frame is not None:
msg = Image()
msg.header = Header()
msg.header.stamp = rospy.Time.now()
msg.header.frame_id = f"camera_{id_cam.lower()}"
msg.height, msg.width = frame.shape[:2]
msg.encoding = "bgr8"
msg.is_bigendian = False
msg.step = frame.strides[0]
msg.data = frame.tobytes()
publicadores[id_cam].publish(msg)
rate.sleep()
if __name__ == '__main__':
try:
publicar_en_ros()
except rospy.ROSInterruptException:
pass
Consideraciones Técnicas
El uso de setFrameSyncMode es crítico para asegurar que los sensores capturen la imagen en el mismo instante temporal, minimizando el desfase en aplicaciones de reconstrucción 3D o robótica móvil. En el modo manual, el ajuste de setManualExposure permite controlar el desenfoque por movimiento (motion blur) limitando el tiempo de integración, lo cual es vital en entornos industriales con iluminación controlada.
Para visualizar los resultados, tras ejecutar el script con Python 3, se puede utilizar la herramienta rviz o image_view de ROS, suscribiéndose a los tópicos definidos (ej. /oak/cam_a/image_raw).