Configuración de parámetros de captura manuales y automáticos en cámaras OAK

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

Etiquetas: depthai ROS computer-vision Python OAK-D

Publicado el 7-18 17:20