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:
- Conecta VCC (3.3 V) del Jetson a VCC del PCA9685. No uses 5 V aquí.
- 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).
- Conecta SDA (J41 pin 3) a SDA del PCA9685, y SCL (J41 pin 5) a SCL del PCA9685.
- Conecta la fuente externa 5–6 V al pin V+ y GND del bus de potencia del PCA9685 (regleta superior).
- Conecta los servos a CH0 (pan) y CH1 (tilt), respetando las señales: señal (PWM), V+ y GND.
- 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
- Verifica toolchain:
- cat /etc/nv_tegra_release debe indicar R35.4.1 (JetPack 5.1.2) y modelo Xavier NX.
- dpkg -l | grep tensorrt debe listar TensorRT 8.5.x.
-
python3 -c ‘import cv2; print(cv2.version)’ → 4.5.0.
-
I2C:
- sudo i2cdetect -y -r 1 muestra 0x40 en la cuadrícula. Si no, revisa VCC, SDA, SCL, GND y alimentación de servos.
-
No debe haber más de una dirección 0x40; si hay varias, comprueba si hay otros dispositivos en el bus.
-
Cámara:
- v4l2-ctl –list-devices muestra “HD Pro Webcam C920” o similar.
- v4l2-ctl -d /dev/video0 –list-formats-ext incluye MJPG: 1280×720@30.
-
gst-launch-1.0 v4l2src device=/dev/video0 ! image/jpeg,framerate=30/1 ! jpegdec ! fakesink -e finaliza sin errores.
-
Test TensorRT (Ruta A):
-
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.
-
Seguimiento por color:
- Ejecuta el script con –display, coloca un objeto verde sólido delante de la C920.
- 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.
- El gimbal debe moverse suavemente hacia el objetivo; cuando el objeto se queda quieto, el jitter debe ser mínimo.
-
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.
-
Estabilidad:
- Mantén el objeto en diferentes posiciones; el sistema debe recentrarlo en <2 s.
-
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.
-
Seguridad:
- 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)
- PCA9685 no aparece en i2cdetect (0x40 ausente):
- Causa: Cableado incorrecto o sin GND común.
-
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).
-
Servos tiemblan (jitter) o se reinician:
- Causa: Fuente de servos insuficiente o caída de tensión por cable fino.
-
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.
-
La cámara no abre con OpenCV/GStreamer:
- Causa: Falta gst-plugins o permisos.
-
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).
-
FPS muy bajo (<15 FPS):
- Causa: Decodificación o conversión lenta, display en pantalla usando X sobrecompuesto, Kp muy alto genera oscilaciones y gasto extra.
-
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).
-
Movimiento invertido (el pan/tilt va al lado contrario):
- Causa: Montaje mecánico o orientación del servo.
-
Solución: Lanza el script con –invert-pan y/o –invert-tilt. También puedes intercambiar servos en canales.
-
Recorrido insuficiente o golpeteo mecánico en extremos:
- Causa: Pulsos de servo fuera del rango seguro o saturación a 0/180°.
-
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.
-
trtexec falla al construir el motor ONNX:
- Causa: Modelo no compatible/descarga corrupta, TensorRT no encuentra capas, out-of-memory.
-
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.
-
Throttling térmico (retrasos, tegrastats reporta throttling o temperaturas altas):
- Causa: MAXN + cargas sostenidas sin ventilación.
- 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
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.




