Caso práctico: Seguimiento de color con pan-tilt en Jetson

Caso práctico: Seguimiento de color con pan-tilt en Jetson — hero

Objetivo y caso de uso

Qué construirás: Un sistema de seguimiento por color en tiempo real que controla una montura pan-tilt con servos vía Adafruit PCA9685 para mantener un objeto coloreado centrado en la imagen de una Logitech C920 en un Jetson Xavier NX. Incluye umbral HSV y lazo de control (P/PI/PID) para correcciones suaves.

Para qué sirve

  • Seguimiento de objetos en demos de visión (p. ej., una pelota verde en un laboratorio).
  • Encuadre automático en streamings/clases, manteniendo un marcador de color en el centro.
  • Robótica educativa: orientar cámaras hacia balizas de color durante navegación.
  • Bancos de prueba de servos con realimentación visual simple (HSV).
  • Prototipos de torretas educativas que siguen objetivos marcados por color.

Resultado esperado

  • 25–30 FPS sostenidos a 1280×720@30 con la C920; latencia visual percibida < 100 ms.
  • Estabilidad del control: error medio de centrado < 10% del ancho/alto tras 2 s de adquisición.
  • Consumo moderado de CPU < 40% en 4 núcleos; baja carga de GPU en el lazo de color (~0–10%).
  • Prueba adicional: ResNet50 FP16 en TensorRT > 100 FPS (capacidad de cómputo disponible).
  • I2C estable con el PCA9685 durante sesiones prolongadas.

Público objetivo: makers, docentes y estudiantes de robótica/visión; Nivel: intermedio.

Arquitectura/flujo: Logitech C920 (USB) → captura V4L2/GStreamer en Jetson → OpenCV: conversión a HSV, umbral, morfología, contorno/centroide → cálculo de error (dx, dy) → controlador P/PI/PID → mapeo a duty cycle → Adafruit PCA9685 por I2C → servos pan/tilt → cámara reorientada; bucle a 25–30 FPS con latencia < 100 ms.

Prerrequisitos (SO y toolchain concreta)

Este caso práctico está probado con la siguiente toolchain en Jetson Xavier NX:

  • JetPack 5.1.2 (L4T R35.4.1)
  • Ubuntu 20.04.6 LTS (Focal)
  • Kernel: 5.10.104-tegra
  • CUDA 11.4.315
  • cuDNN 8.6.0.166
  • TensorRT 8.5.2.2
  • OpenCV 4.5.0 (instalada por JetPack; compatible con GStreamer)
  • GStreamer 1.16.3
  • Python 3.8.10
  • Herramientas Jetson: nvpmodel, jetson_clocks, tegrastats
  • Adafruit_PCA9685 1.0.1 (Python)
  • smbus2 0.4.3

Verifica tu versión de JetPack y componentes:

# Modelo de Jetson y L4T/JetPack
cat /etc/nv_tegra_release

# Kernel
uname -a

# GPU/AI stack presentes
dpkg -l | grep -E 'nvidia|tensorrt|cuda'

# OpenCV y Python
python3 -c "import sys, cv2; print(sys.version); print(cv2.__version__)"

# GStreamer
gst-launch-1.0 --version

Pauta de AI acelerada (se elige UNA): A) TensorRT + ONNX. Lo usaremos para un test de rendimiento de GPU con trtexec, independiente del lazo de color, a fin de validar aceleración hardware con métricas reproducibles. El seguimiento por color (HSV) se ejecuta con OpenCV en CPU.

Materiales

  • Jetson Xavier NX (Developer Kit) con JetPack 5.1.2.
  • Webcam USB: Logitech C920 (UVC, MJPEG/H.264).
  • Controlador PWM: Adafruit PCA9685 (16-Channel 12-bit PWM/Servo Driver, dirección I2C por defecto 0x40).
  • Dos servos (para pan y tilt), típicos SG90/MG90S o estándar 9g/metal.
  • Fuente externa 5–6 V DC para servos (capaz de suministrar 1–3 A según servos).
  • Cables Dupont macho-hembra para I2C (SDA/SCL), GND y VCC (lógica).
  • Cable USB para la C920.
  • Conjunto pan-tilt mecánico compatible con servos.
  • Opcional pero recomendado: disipador/ventilador en el Xavier NX.

Nota importante: No alimente los servos desde el 5 V del Jetson. Use una fuente externa y comparta GND con el Jetson.

Preparación y conexión

Tabla de conexiones (Jetson Xavier NX 40 pines → PCA9685 y servos)

Elemento Jetson (J41 header) PCA9685 Descripción
Lógica 3V3 Pin 1 (3.3 V) VCC Alimentación lógica del PCA9685 (no servos)
GND (común) Pin 6 (GND) GND Tierra común para lógica y servos
I2C SDA Pin 3 (I2C1 SDA) SDA Datos I2C, bus 1
I2C SCL Pin 5 (I2C1 SCL) SCL Reloj I2C, bus 1
Alimentación servos Fuente 5–6 V (externa) V+ Potencia para los servos (no conectar al 5 V del Jetson)
Servo PAN CH0 (PWM), V+, GND Señal a canal 0; rojo a V+, marrón/negro a GND, amarillo/naranja a PWM
Servo TILT CH1 (PWM), V+, GND Señal a canal 1; mismo cableado

Pasos:

  1. Conecta VCC (3.3 V) del Jetson a VCC del PCA9685. No uses 5 V aquí.
  2. Conecta GND del Jetson al GND del PCA9685. Conecta también GND de la fuente externa de servos al mismo GND del PCA9685 (todas las tierras comunes).
  3. Conecta SDA (J41 pin 3) a SDA del PCA9685, y SCL (J41 pin 5) a SCL del PCA9685.
  4. Conecta la fuente externa 5–6 V al pin V+ y GND del bus de potencia del PCA9685 (regleta superior).
  5. Conecta los servos a CH0 (pan) y CH1 (tilt), respetando las señales: señal (PWM), V+ y GND.
  6. Conecta la Logitech C920 a un puerto USB 3.0 del Jetson.

Verificación rápida del bus I2C:

sudo apt-get update
sudo apt-get install -y i2c-tools
sudo i2cdetect -y -r 1
# Debe aparecer "40" indicando el PCA9685 en 0x40 (bus 1).

Verificación de la cámara:

v4l2-ctl --list-devices
v4l2-ctl -d /dev/video0 --list-formats-ext
# Busca MJPEG a 1280x720@30 o 1920x1080@30 en la C920.

Código completo

A continuación, un script en Python que:

  • Abre la Logitech C920 vía GStreamer con MJPEG a 1280×720@30.
  • Convierte a HSV y segmenta un color (por defecto, verde, ajustable por CLI).
  • Encuentra el contorno más grande y calcula el centroide.
  • Aplica control proporcional simple (P) para pan y tilt.
  • Genera PWM a 50 Hz con el PCA9685 para mover los servos.

Guarda el archivo como opencv_color_pan_tilt.py.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import argparse
import time
import sys
import math
import numpy as np
import cv2
from smbus2 import SMBus
import Adafruit_PCA9685

def build_gst_pipeline(device="/dev/video0", width=1280, height=720, fps=30):
    # Logitech C920 genera MJPEG; pipeline eficiente usando decodificador por CPU
    # Nota: appsink con BGR para OpenCV
    pipeline = (
        f"v4l2src device={device} ! "
        f"image/jpeg,framerate={fps}/1,width={width},height={height} ! "
        "jpegdec ! "
        "videoconvert ! "
        "video/x-raw,format=BGR ! "
        "appsink drop=true sync=false"
    )
    return pipeline

def us_to_ticks(pulse_us, freq_hz=50.0):
    # PCA9685: 12-bit (4096 ticks), periodo = 1/freq
    period_us = 1e6 / freq_hz
    ticks = int((pulse_us / period_us) * 4096)
    return max(0, min(4095, ticks))

def angle_to_us(angle_deg, us_min=500, us_max=2500):
    angle = max(0.0, min(180.0, float(angle_deg)))
    return int(us_min + (angle / 180.0) * (us_max - us_min))

class PanTiltController:
    def __init__(self, i2c_bus=1, address=0x40, freq_hz=50,
                 pan_channel=0, tilt_channel=1,
                 us_min=500, us_max=2500,
                 invert_pan=False, invert_tilt=False):
        self.pwm = Adafruit_PCA9685.PCA9685(address=address, busnum=i2c_bus)
        self.pwm.set_pwm_freq(freq_hz)
        self.freq_hz = freq_hz
        self.pan_ch = pan_channel
        self.tilt_ch = tilt_channel
        self.us_min = us_min
        self.us_max = us_max
        self.invert_pan = invert_pan
        self.invert_tilt = invert_tilt
        # Posición inicial al centro
        self.pan = 90.0
        self.tilt = 90.0
        self.commit()

    def commit(self):
        pan_ang = 180.0 - self.pan if self.invert_pan else self.pan
        tilt_ang = 180.0 - self.tilt if self.invert_tilt else self.tilt
        pan_ticks = us_to_ticks(angle_to_us(pan_ang, self.us_min, self.us_max), self.freq_hz)
        tilt_ticks = us_to_ticks(angle_to_us(tilt_ang, self.us_min, self.us_max), self.freq_hz)
        self.pwm.set_pwm(self.pan_ch, 0, pan_ticks)
        self.pwm.set_pwm(self.tilt_ch, 0, tilt_ticks)

    def nudge(self, d_pan=0.0, d_tilt=0.0):
        self.pan = float(np.clip(self.pan + d_pan, 0.0, 180.0))
        self.tilt = float(np.clip(self.tilt + d_tilt, 0.0, 180.0))
        self.commit()

def parse_args():
    ap = argparse.ArgumentParser(description="Seguimiento por color HSV con control pan-tilt (PCA9685).")
    ap.add_argument("--device", type=str, default="/dev/video0")
    ap.add_argument("--width", type=int, default=1280)
    ap.add_argument("--height", type=int, default=720)
    ap.add_argument("--fps", type=int, default=30)
    # HSV en OpenCV: H [0,179], S [0,255], V [0,255]
    ap.add_argument("--hmin", type=int, default=35, help="Hue min (verde ~35)")
    ap.add_argument("--hmax", type=int, default=85, help="Hue max (verde ~85)")
    ap.add_argument("--smin", type=int, default=80)
    ap.add_argument("--vmin", type=int, default=80)
    ap.add_argument("--i2c-bus", type=int, default=1)
    ap.add_argument("--pca-addr", type=lambda x: int(x, 0), default=0x40)  # admite 0x40
    ap.add_argument("--pan-ch", type=int, default=0)
    ap.add_argument("--tilt-ch", type=int, default=1)
    ap.add_argument("--us-min", type=int, default=500)
    ap.add_argument("--us-max", type=int, default=2500)
    ap.add_argument("--invert-pan", action="store_true")
    ap.add_argument("--invert-tilt", action="store_true")
    ap.add_argument("--kp-pan", type=float, default=0.15, help="Ganancia P en deg/px (pan)")
    ap.add_argument("--kp-tilt", type=float, default=0.15, help="Ganancia P en deg/px (tilt)")
    ap.add_argument("--display", action="store_true", help="Mostrar ventana con overlay")
    ap.add_argument("--morph", type=int, default=3, help="Kernel morfológico (ímpar)")
    return ap.parse_args()

def main():
    args = parse_args()

    # Controlador pan-tilt (PCA9685)
    try:
        pt = PanTiltController(
            i2c_bus=args.i2c_bus,
            address=args.pca_addr,
            freq_hz=50,
            pan_channel=args.pan_ch,
            tilt_channel=args.tilt_ch,
            us_min=args.us_min,
            us_max=args.us_max,
            invert_pan=args.invert_pan,
            invert_tilt=args.invert_tilt
        )
    except Exception as e:
        print(f"ERROR: no se pudo inicializar PCA9685 en I2C bus {args.i2c_bus}, addr {hex(args.pca_addr)}: {e}")
        sys.exit(1)

    # Captura de cámara
    gst = build_gst_pipeline(args.device, args.width, args.height, args.fps)
    cap = cv2.VideoCapture(gst, cv2.CAP_GSTREAMER)
    if not cap.isOpened():
        print("ERROR: no se pudo abrir la cámara con GStreamer. Revisa gstreamer1.0 y permisos.")
        sys.exit(2)

    # Prepara filtros
    kernel_size = max(1, args.morph)
    if kernel_size % 2 == 0:
        kernel_size += 1
    kernel = np.ones((kernel_size, kernel_size), np.uint8)

    t0 = time.time()
    frames = 0
    fps = 0.0
    font = cv2.FONT_HERSHEY_SIMPLEX

    try:
        while True:
            ok, frame = cap.read()
            if not ok:
                print("WARN: frame inválido, reintentando...")
                time.sleep(0.01)
                continue

            frames += 1
            if frames % 20 == 0:
                dt = time.time() - t0
                fps = frames / dt if dt > 0 else 0.0

            # Procesado HSV
            hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
            lower = np.array([args.hmin, args.smin, args.vmin], dtype=np.uint8)
            upper = np.array([args.hmax, 255, 255], dtype=np.uint8)
            mask = cv2.inRange(hsv, lower, upper)
            # Morfología para limpiar ruido
            mask = cv2.erode(mask, kernel, iterations=1)
            mask = cv2.dilate(mask, kernel, iterations=2)

            # Contornos
            contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
            cx = cy = None
            radius = 0
            if contours:
                c = max(contours, key=cv2.contourArea)
                (x, y), radius = cv2.minEnclosingCircle(c)
                M = cv2.moments(c)
                if M["m00"] > 1e-6:
                    cx = int(M["m10"] / M["m00"])
                    cy = int(M["m01"] / M["m00"])

            # Control P
            h, w = frame.shape[:2]
            center_x, center_y = w // 2, h // 2
            if cx is not None and cy is not None and radius > 5:
                err_x = (cx - center_x)  # + derecha
                err_y = (cy - center_y)  # + abajo
                d_pan = -args.kp_pan * err_x  # convención: pan + hacia derecha
                d_tilt = args.kp_tilt * err_y  # tilt + hacia abajo
                pt.nudge(d_pan, d_tilt)

            if args.display:
                # Overlay
                cv2.circle(frame, (center_x, center_y), 5, (255, 255, 255), -1)
                if cx is not None and cy is not None and radius > 5:
                    cv2.circle(frame, (int(cx), int(cy)), int(radius), (0, 255, 0), 2)
                    cv2.line(frame, (center_x, center_y), (int(cx), int(cy)), (0, 0, 255), 1)
                cv2.putText(frame, f"FPS: {fps:.1f}", (10, 25), font, 0.8, (0, 255, 255), 2)
                cv2.putText(frame, f"Pan: {pt.pan:.1f} Tilt: {pt.tilt:.1f}", (10, 50), font, 0.7, (0, 255, 0), 2)
                cv2.imshow("opencv-color-pan-tilt-tracking", frame)
                key = cv2.waitKey(1) & 0xFF
                if key == 27 or key == ord('q'):
                    break
            else:
                # Sin display: dormir ligeramente para evitar 100% CPU
                time.sleep(0.001)

    except KeyboardInterrupt:
        pass
    finally:
        cap.release()
        cv2.destroyAllWindows()
        # Lleva a posición central al salir
        pt.pan = 90; pt.tilt = 90; pt.commit()
        print("Salida limpia.")

if __name__ == "__main__":
    main()

Puntos clave del código:

  • GStreamer con v4l2src y jpegdec asegura baja latencia con la C920 (que entrega MJPEG por hardware).
  • Control proporcional P simple evita oscilaciones fuertes; la ganancia en deg/px se puede ajustar por CLI.
  • Mapas de ángulo a microsegundos y a ticks del PCA9685 (50 Hz) para servos de hobby.
  • Opción –invert-pan/–invert-tilt por si el montaje invierte ejes.

Como validación de GPU acelerada con TensorRT (ruta A), usaremos trtexec con ResNet50 en FP16. Guarda el siguiente micro-script opcional (no imprescindible para el control pan-tilt), solo para automatizar el test y registrar métricas:

# test_trt_resnet50.sh
set -e
cd ~/jetson_trt_test || mkdir -p ~/jetson_trt_test && cd ~/jetson_trt_test
wget -O resnet50-v1-12.onnx https://github.com/onnx/models/raw/main/vision/classification/resnet/model/resnet50-v1-12.onnx
/usr/src/tensorrt/bin/trtexec --onnx=resnet50-v1-12.onnx --fp16 --workspace=1024 --saveEngine=resnet50_fp16.plan
/usr/src/tensorrt/bin/trtexec --loadEngine=resnet50_fp16.plan --iterations=200 --avgRuns=100 --useSpinWait

Se construye un motor FP16 y se mide rendimiento medio.

Compilación/flash/ejecución

No hay compilación/flash (Python), pero sí instalación de dependencias y ejecución ordenada.

1) Preparar entorno y dependencias:

# Actualiza repos e instala paquetes necesarios
sudo apt-get update
sudo apt-get install -y python3-pip python3-opencv v4l-utils \
    gstreamer1.0-tools gstreamer1.0-plugins-good gstreamer1.0-plugins-bad \
    gstreamer1.0-plugins-ugly gstreamer1.0-libav i2c-tools

# Librerías Python exactas para PCA9685 e I2C
pip3 install --no-cache-dir Adafruit_PCA9685==1.0.1 smbus2==0.4.3 numpy==1.23.5

2) Comprobar el modo de potencia y reloj (opcional, para rendimiento consistente):

# Ver modo actual
sudo nvpmodel -q
# Establecer MAXN (modo 0) y fijar relojes (cuidado con termals)
sudo nvpmodel -m 0
sudo jetson_clocks

3) Probar TensorRT (ruta A) para validar GPU:

bash ./test_trt_resnet50.sh
# Observa FPS o ms/iter en la salida.

4) Verificar cámara y formato:

# Ver resoluciones disponibles para MJPEG
v4l2-ctl -d /dev/video0 --list-formats-ext
# Opcional: fijar MJPEG 1280x720@30 en la C920
v4l2-ctl -d /dev/video0 --set-fmt-video=width=1280,height=720,pixelformat=MJPG

5) Verificar I2C y PCA9685:

sudo i2cdetect -y -r 1
# Debe ver 0x40. Si no aparece, revisar cableado y alimentación.

6) Ejecutar el seguimiento por color:

# Ejecuta sin ventana (headless)
python3 opencv_color_pan_tilt.py --device /dev/video0 --width 1280 --height 720 --fps 30 \
  --hmin 35 --hmax 85 --smin 80 --vmin 80 --i2c-bus 1 --pca-addr 0x40 --pan-ch 0 --tilt-ch 1

# Ejecuta con ventana de visualización
python3 opencv_color_pan_tilt.py --display --device /dev/video0 --width 1280 --height 720 --fps 30 \
  --hmin 35 --hmax 85 --smin 80 --vmin 80 --i2c-bus 1 --pca-addr 0x40 --pan-ch 0 --tilt-ch 1

7) Medición de rendimiento y consumo durante la ejecución:

# En otra terminal, observa recursos
sudo tegrastats

8) Al terminar, puedes revertir los relojes (opcional):

# Resetear jetson_clocks al estado dinámico (un reboot también lo hace)
sudo systemctl restart nvfancontrol || true
# Cambiar a modo de menor consumo si lo deseas (consultar modos disponibles)
sudo nvpmodel -q

Validación paso a paso

  1. Verifica toolchain:
  2. cat /etc/nv_tegra_release debe indicar R35.4.1 (JetPack 5.1.2) y modelo Xavier NX.
  3. dpkg -l | grep tensorrt debe listar TensorRT 8.5.x.
  4. python3 -c ‘import cv2; print(cv2.version)’ → 4.5.0.

  5. I2C:

  6. sudo i2cdetect -y -r 1 muestra 0x40 en la cuadrícula. Si no, revisa VCC, SDA, SCL, GND y alimentación de servos.
  7. No debe haber más de una dirección 0x40; si hay varias, comprueba si hay otros dispositivos en el bus.

  8. Cámara:

  9. v4l2-ctl –list-devices muestra “HD Pro Webcam C920” o similar.
  10. v4l2-ctl -d /dev/video0 –list-formats-ext incluye MJPG: 1280×720@30.
  11. gst-launch-1.0 v4l2src device=/dev/video0 ! image/jpeg,framerate=30/1 ! jpegdec ! fakesink -e finaliza sin errores.

  12. Test TensorRT (Ruta A):

  13. trtexec debe construir el motor en ~2–10 s y reportar tiempo inferido. Valores de referencia típicos en Xavier NX (MAXN, FP16):

    • ResNet50 FP16: ~3.5–7 ms por inferencia (≈140–285 FPS) en GPU.
    • Si usas DLA (–useDLACore=0 –allowGPUFallback) verás más latencia pero descarga la GPU.
  14. Seguimiento por color:

  15. Ejecuta el script con –display, coloca un objeto verde sólido delante de la C920.
  16. Métricas esperadas (1280×720@30):
    • FPS ≈ 25–30 con overlay activado; sin overlay puede ser mayor.
    • CPU total ≈ 25–40% (tegrastats).
    • GPU ≈ 0–10% (procesado CPU), EMC moderado.
  17. El gimbal debe moverse suavemente hacia el objetivo; cuando el objeto se queda quieto, el jitter debe ser mínimo.
  18. El overlay mostrará:

    • Un círculo verde alrededor del contorno detectado.
    • Una línea del centro de imagen al centroide del objeto.
    • Texto de FPS y ángulos actuales Pan/Tilt.
  19. Estabilidad:

  20. Mantén el objeto en diferentes posiciones; el sistema debe recentrarlo en <2 s.
  21. Error de centrado: toma varios cuadros y estima |cx – center_x|/width; la media debería ser <10% si Kp está bien ajustado y la mecánica no roza.

  22. Seguridad:

  23. Observa temperatura (tegrastats muestra temp GPU/CPU). Evita throttling; si aparece, considera reducir FPS, resolución o quitar jetson_clocks.

Troubleshooting (errores típicos y soluciones)

  1. PCA9685 no aparece en i2cdetect (0x40 ausente):
  2. Causa: Cableado incorrecto o sin GND común.
  3. Solución: Verifica que VCC (3.3 V) vaya al pin VCC del PCA9685 y que SDA/SCL estén en pines 3 y 5 del Jetson. Comprueba GND común entre Jetson, PCA9685 y fuente de servos. Asegura que el bus 1 está habilitado (lo está por defecto en Xavier NX DevKit).

  4. Servos tiemblan (jitter) o se reinician:

  5. Causa: Fuente de servos insuficiente o caída de tensión por cable fino.
  6. Solución: Usa una fuente de 5–6 V con suficiente corriente (≥2 A si son dos servos). Usa cables cortos y de sección adecuada. Añade condensador electrolítico (ej. 470–1000 µF) entre V+ y GND cerca del PCA9685.

  7. La cámara no abre con OpenCV/GStreamer:

  8. Causa: Falta gst-plugins o permisos.
  9. Solución: Instala gstreamer1.0-plugins-{good,bad,ugly} y gstreamer1.0-libav. Ejecuta con cv2.CAP_GSTREAMER. Prueba pipeline con gst-launch-1.0. Verifica que /dev/video0 no esté en uso (cierra otras apps).

  10. FPS muy bajo (<15 FPS):

  11. Causa: Decodificación o conversión lenta, display en pantalla usando X sobrecompuesto, Kp muy alto genera oscilaciones y gasto extra.
  12. Solución: Usa MJPEG y jpegdec (evita YUY2 si la CPU se satura). Desactiva –display para medir rendimiento puro. Reduce resolución a 640×480. Activa MAXN y jetson_clocks (con cuidado térmico).

  13. Movimiento invertido (el pan/tilt va al lado contrario):

  14. Causa: Montaje mecánico o orientación del servo.
  15. Solución: Lanza el script con –invert-pan y/o –invert-tilt. También puedes intercambiar servos en canales.

  16. Recorrido insuficiente o golpeteo mecánico en extremos:

  17. Causa: Pulsos de servo fuera del rango seguro o saturación a 0/180°.
  18. Solución: Ajusta –us-min/–us-max según tus servos (común: 600–2400 µs en lugar de 500–2500). Limita Kp y establece topes <0/180 si tu mecánica lo requiere.

  19. trtexec falla al construir el motor ONNX:

  20. Causa: Modelo no compatible/descarga corrupta, TensorRT no encuentra capas, out-of-memory.
  21. Solución: Re-descarga el ONNX. Usa –fp16 y reduce workspace: –workspace=512. Si persiste, prueba otro modelo ONNX de clasificación oficial. Verifica TensorRT 8.5.x instalado.

  22. Throttling térmico (retrasos, tegrastats reporta throttling o temperaturas altas):

  23. Causa: MAXN + cargas sostenidas sin ventilación.
  24. Solución: Añade ventilación/disipación. Reduce frecuencia (omite jetson_clocks). Baja resolución/FPS.

Mejoras/variantes

  • Filtros adaptativos:
  • Añade trackbars para ajustar H/S/V en tiempo real según iluminación.
  • Estima el color objetivo haciendo click en la imagen para adaptar el rango HSV (muestreo local).

  • Control más robusto:

  • Implementa un controlador PID con integral limitada y derivativa filtrada para reducir overshoot.
  • Añade zona muerta (deadband) en px para evitar microajustes cuando el error es pequeño.

  • Suavizado visual:

  • Usa filtro exponencial en la posición objetivo (cx, cy) para reducir ruido de contornos.
  • Interpola el setpoint de servos (slew-rate limiting) para movimientos más suaves.

  • Pipeline acelerado:

  • Cambia a GStreamer con nvvidconv para otro tipo de cámaras/formato; para C920 (MJPEG) la ganancia es menor, pero puedes probar:

    • v4l2src ! image/jpeg ! jpegdec ! nvvidconv ! video/x-raw,format=BGRx ! videoconvert ! BGR ! appsink
  • Sustituir color por IA:

  • Reemplaza la segmentación por color por detección con YOLOv5/YOLOv8 en TensorRT, y usa la caja con mayor confianza como objetivo. Mantén el mismo lazo de control pan-tilt.
  • Si quieres descargar la GPU al DLA, usa –useDLACore=0 en motores compatibles.

  • Seguridad y robustez:

  • Añade watchdog para recentrar servos si no se detecta objetivo durante X segundos.
  • Persiste calibraciones en un archivo YAML (rangos HSV, Kp, límites servo).

  • Integración:

  • Publica estado (FPS, pan, tilt, error) por MQTT para monitoreo remoto.
  • Añade una API REST mínima (Flask) para cambiar parámetros en caliente.

Checklist de verificación

  • [ ] JetPack 5.1.2 (L4T R35.4.1) y Ubuntu 20.04.6 confirmados en el Xavier NX.
  • [ ] CUDA 11.4.315, cuDNN 8.6.0.166, TensorRT 8.5.2.2 presentes (dpkg).
  • [ ] OpenCV 4.5.0 y GStreamer 1.16.3 instalados y funcionales.
  • [ ] Logitech C920 detectada en /dev/video0 y ofrece MJPG 1280×720@30.
  • [ ] PCA9685 visible en I2C bus 1 con dirección 0x40.
  • [ ] Fuente de 5–6 V dedicada para servos, GND común con Jetson.
  • [ ] Script opencv_color_pan_tilt.py ejecuta y muestra FPS, pan/tilt y contorno cuando hay un objeto verde.
  • [ ] Respuesta estable: objeto centrado en <2 s, sin jitter excesivo al estabilizar.
  • [ ] Métricas con tegrastats dentro de lo esperado (CPU <40%, sin throttling).
  • [ ] Test TensorRT con trtexec completado y FPS/latencias coherentes para ResNet50 FP16.

Apéndice: Tabla de toolchain y versiones

Componente Versión exacta (referencia) Comando de verificación
JetPack (L4T) 5.1.2 (R35.4.1) cat /etc/nv_tegra_release
Ubuntu 20.04.6 LTS lsb_release -a
Kernel 5.10.104-tegra uname -a
CUDA 11.4.315 nvcc –version (si instalado) o dpkg -l
cuDNN 8.6.0.166 dpkg -l
TensorRT 8.5.2.2 /usr/src/tensorrt/bin/trtexec –version
OpenCV (Python) 4.5.0 python3 -c «import cv2; print(cv2.version
GStreamer 1.16.3 gst-launch-1.0 –version
Python 3.8.10 python3 –version
Adafruit_PCA9685 1.0.1 python3 -c «import Adafruit_PCA9685, pkgutil; import pkg_resources; print(pkg_resources.get_distribution(‘Adafruit_PCA9685’))»
smbus2 0.4.3 python3 -c «import smbus2, pkg_resources; print(pkg_resources.get_distribution(‘smbus2’))»

Notas finales:

  • Todo el flujo está centrado en el modelo concreto: Jetson Xavier NX + Logitech C920 + Adafruit PCA9685. Los comandos, conexiones y código han sido escritos para ese hardware.
  • Si tu entorno difiere en versión exacta (por ejemplo, JetPack 5.1.3), ajusta únicamente los números de versión en la sección de verificación; el procedimiento permanece igual.
  • Este proyecto (opencv-color-pan-tilt-tracking) se ha enfocado a segmentación por color HSV por su valor didáctico y facilidad de calibración, manteniendo además una validación de GPU con TensorRT siguiendo la pauta A del toolchain.

Encuentra este producto y/o libros sobre este tema en Amazon

Ir a Amazon

Como afiliado de Amazon, gano con las compras que cumplan los requisitos. Si compras a través de este enlace, ayudas a mantener este proyecto.

Quiz rápido

Pregunta 1: ¿Cuál es el objetivo principal del sistema descrito?




Pregunta 2: ¿Qué tipo de cámara se utiliza en el sistema?




Pregunta 3: ¿Qué protocolo se utiliza para la comunicación con el PCA9685?




Pregunta 4: ¿Cuál es la latencia visual percibida esperada en el sistema?




Pregunta 5: ¿Qué tipo de control se implementa para las correcciones suaves?




Pregunta 6: ¿Cuál es el consumo moderado de CPU esperado en el sistema?




Pregunta 7: ¿Qué resolución se espera alcanzar en el sistema?




Pregunta 8: ¿Cuál es el público objetivo del sistema?




Pregunta 9: ¿Qué tipo de error se espera en el centrado tras 2 segundos de adquisición?




Pregunta 10: ¿Qué aplicaciones educativas se pueden desarrollar con este sistema?




Carlos Núñez Zorrilla
Carlos Núñez Zorrilla
Electronics & Computer Engineer

Ingeniero Superior en Electrónica de Telecomunicaciones e Ingeniero en Informática (titulaciones oficiales en España).

Sígueme:
Scroll al inicio