Objetivo y caso de uso
Qué construirás: Un sistema PTZ (pan-tilt con zoom digital) que detecta y sigue objetos en tiempo real con DeepStream sobre GPU en un Jetson Xavier NX, usando una cámara CSI Arducam IMX477 y servos controlados por un PCA9685 vía I2C. Integrará inferencia (TensorRT), tracking y control de servos con suavizado y zoom.
Para qué sirve
- Vigilancia activa: mantener a una persona centrada al cruzar un pasillo o la entrada de un edificio.
- Robótica móvil: un robot que conserva una señal o casco en el centro mientras navega por un almacén.
- Demos de visión embebida: mostrar IA acelerada por GPU y mecatrónica coordinada en ferias o laboratorios.
- Fotogrametría/inspección: seguir un componente en una línea de producción ajustando el encuadre al moverse.
Resultado esperado
- ≥ 25 FPS a 1920×1080 en modo MAXN con DeepStream (batch-size=1), validado con perf-measurement.
- Latencia de seguimiento < 200 ms desde centroide detectado hasta movimiento de servo (con timestamps en logs).
- Precisión de centrado: error angular equivalente < ±3% del ancho/alto del encuadre (centroide cerca de la cruz central).
- Uso de GPU: 45–65% a 1080p con TensorRT FP16; actualización de servos a 50–100 Hz sin pérdida de frames.
Público objetivo: integradores de visión embebida, equipos de I+D, makers/roboticistas; Nivel: intermedio–avanzado
Arquitectura/flujo: IMX477 → DeepStream (nvarguscamerasrc → nvvideoconvert → nvinfer TensorRT FP16 → nvtracker) → sonda que obtiene el centroide del bounding box → controlador (PID + suavizado) que convierte error de píxeles a ángulos pan/tilt y zoom digital → comandos I2C al PCA9685 → servos; ejecución en Jetson Xavier NX en MAXN, con telemetría de FPS/latencia/%GPU.
Prerrequisitos (SO y toolchain exacta)
Verifica que tu entorno corresponde a estas versiones exactas:
- Hardware
- Jetson Xavier NX Developer Kit
- Cámara CSI Arducam IMX477 (Raspberry Pi HQ compatible para Jetson, driver instalado)
- Controlador PWM PCA9685 (placa 16 canales)
- Sistema operativo y toolchain
- Ubuntu 20.04.6 LTS (aarch64) provisto por JetPack
- NVIDIA JetPack 5.1.2 (L4T R35.4.1)
- CUDA 11.4.315
- TensorRT 8.5.2-1+cuda11.4
- cuDNN 8.6.x (instalado con JetPack)
- DeepStream SDK 6.2
- GStreamer 1.16.3
- Python 3.8.10
- OpenCV 4.5.4 (de JetPack)
- DeepStream Python bindings pyds 1.1.0 (para DS 6.2)
- Librerías I2C/servos:
- adafruit-circuitpython-servokit 1.4.0
- adafruit-circuitpython-pca9685 3.4.8
- smbus2 0.4.3
Comandos para verificar:
# Verifica JetPack/L4T
cat /etc/nv_tegra_release
# Ejemplo esperado: # R35 (release), REVISION: 4.1, GCID: ...
uname -a
# Verifica paquetes NVIDIA clave
dpkg -l | grep -E 'nvidia|tensorrt|deepstream'
# Si tienes jetson_release instalado:
jetson_release -v
# Verifica versiones de Python y pyds
python3 -V
python3 -c "import gi, sys; import pyds; print('pyds OK')"
# Verifica GStreamer
gst-inspect-1.0 | head -n 5
Recomendaciones de rendimiento y potencia para las pruebas:
# Modo potencia MAXN (Xavier NX: modo 0)
sudo nvpmodel -q
sudo nvpmodel -m 0
sudo jetson_clocks
# Lanza tegrastats en otra terminal para medir
sudo tegrastats
Advertencia: MAXN + jetson_clocks elevan consumo térmico; asegúrate de ventilación adecuada.
Materiales
- Jetson Xavier NX Developer Kit + Arducam IMX477 + PCA9685 PWM Driver
- 2 servos (micro-servos tipo MG90S/MG995 o pan-tilt bracket compatible; torque acorde a peso de la IMX477)
- Fuente de 5–6 V DC dedicada para servos (capaz de suministrar 2–3 A pico), común a GND del Jetson
- Jumpers Dupont macho-hembra
- Tornillería/soporte pan-tilt para montar la cámara
- Tarjeta microSD UHS-I (si usas variante SD) o eMMC configurada con JetPack 5.1.2
- Cables CSI para IMX477 (incluido con Arducam)
Preparación y conexión
Habilita y prueba I2C
sudo apt update
sudo apt install -y i2c-tools python3-smbus python3-pip python3-gi python3-gi-cairo \
gir1.2-gst-plugins-base-1.0 gir1.2-gstreamer-1.0
sudo usermod -aG i2c $USER
newgrp i2c
# Descubre la dirección del PCA9685 (normalmente 0x40)
i2cdetect -y -r 1
Salida esperada: un “40” en el mapa (bus 1).
Conexión eléctrica
- Conecta la cámara Arducam IMX477 al conector CSI del Xavier NX.
- Conecta el PCA9685 a I2C-1 del header de 40 pines del Xavier NX.
- Conecta los servos a canales 0 (Pan) y 1 (Tilt) del PCA9685.
- Usa fuente separada para V+ (servos). Une la masa (GND) del Jetson y la del PCA9685/fuente.
Tabla de pines (Jetson Xavier NX 40-pin header ↔ PCA9685/Servos)
| Función | Jetson Xavier NX (pin header) | PCA9685 / Servo | Notas |
|---|---|---|---|
| I2C1 SDA | Pin 3 (GPIO2_SDA.1) | SDA | Bus I2C-1 |
| I2C1 SCL | Pin 5 (GPIO2_SCL.1) | SCL | Bus I2C-1 |
| 3.3 V lógica | Pin 1 o 17 | VCC (lógica PCA9685) | No alimentar servos con 3.3 V |
| GND | Pin 9, 6, 14, 20, etc. | GND | Masa común entre Jetson y PCA9685/servos |
| 5–6 V servos | Fuente externa V+ | V+ (barral servos) | No conectar a 5 V del Jetson; usar fuente dedicada |
| Servo Pan señal | — | Canal 0 (PWM0) | Cable de señal (normalmente amarillo/blanco) |
| Servo Tilt señal | — | Canal 1 (PWM1) |
Prueba de la cámara
Prueba rápida de la cámara CSI IMX477:
# Previsualización sin DeepStream (GStreamer)
gst-launch-1.0 nvarguscamerasrc sensor-id=0 ! \
'video/x-raw(memory:NVMM),width=1920,height=1080,framerate=30/1' ! \
nvvideoconvert ! 'video/x-raw,format=BGRx' ! videoconvert ! autovideosink
Deberías ver vídeo en vivo a ~30 FPS.
Código completo
Elegimos DeepStream (ruta B) para inferencia acelerada por GPU, con un detector primario ligero (ResNet10) y un tracker IOU. Un probe en Python (pyds) extrae bounding boxes y envía comandos a los servos vía PCA9685. Añadimos zoom digital (cropping virtual) como parámetro dinámico (indicador y cálculo en overlay).
Archivos de configuración de DeepStream
Crea pgie_resnet10_config.txt (nvinfer para detector primario). Usamos el modelo incluido en DeepStream.
# pgie_resnet10_config.txt
[property]
gpu-id=0
net-scale-factor=0.0039215697906911373
model-file=/opt/nvidia/deepstream/deepstream/samples/models/Primary_Detector/resnet10.caffemodel
proto-file=/opt/nvidia/deepstream/deepstream/samples/models/Primary_Detector/resnet10.prototxt
labelfile-path=/opt/nvidia/deepstream/deepstream/samples/models/Primary_Detector/labels.txt
# Precision FP16 para Xavier NX
network-mode=1
batch-size=1
interval=0
gie-unique-id=1
process-mode=1
maintain-aspect-ratio=1
# Umbral de detección inicial (ajustable)
pre-cluster-threshold=0.4
# Parseador universal para ResNet10 (no YOLO)
parse-bbox-func-name=NvDsInferParseCustomResnet
custom-lib-path=/opt/nvidia/deepstream/deepstream/lib/libnvds_infercustomparser.so
[class-attrs-all]
nms-iou-threshold=0.5
topk=300
threshold=0.4
Crea tracker_iou_config.yml (para nvtracker IOU simple):
# tracker_iou_config.yml
tracker-width: 640
tracker-height: 384
ll-lib-file: /opt/nvidia/deepstream/deepstream/lib/libnvds_iou.so
# Parámetros IOU básicos
iou-threshold: 0.4
min-det-confidence: 0.4
max-shadow-tracking-age: 30
min-tracked-points: 1
Nota: algunas builds de DeepStream esperan este YAML como “ll-config-file”; usaremos propiedades del elemento nvtracker desde Python y le pasaremos este fichero según corresponda.
Aplicación principal en Python (DeepStream + PCA9685)
Crea ptz_deepstream_xaviernx.py con el siguiente contenido:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys, os, time, math
import gi
gi.require_version('Gst', '1.0')
gi.require_version('GObject', '2.0')
from gi.repository import Gst, GObject
import pyds
# Servos PCA9685
from adafruit_servokit import ServoKit
# Parámetros de cámara y PTZ
FRAME_W, FRAME_H, FPS = 1920, 1080, 30
# Estimación HFOV/VFOV de IMX477 con lente estándar; calibra si usas otra óptica
HFOV_DEG, VFOV_DEG = 68.0, 41.0
# PID simplificado (P+D opcional)
Kp_pan = 45.0 # deg por error normalizado (ajustar)
Kd_pan = 0.0
Kp_tilt = 35.0
Kd_tilt = 0.0
# Límites de servos
PAN_MIN, PAN_MAX = 10.0, 170.0
TILT_MIN, TILT_MAX = 15.0, 165.0
# Zoom digital (factor 1.0 = sin zoom)
ZOOM_MIN, ZOOM_MAX = 1.0, 2.0
zoom_factor = 1.0
# Índices de canales en PCA9685
PAN_CH, TILT_CH = 0, 1
# Inicializa GStreamer
Gst.init(None)
class PTZController:
def __init__(self, i2c_channels=16, i2c_busnum=1, freq=50):
# ServoKit autodetecta bus 1 en Jetson
self.kit = ServoKit(channels=i2c_channels)
# Frecuencia PWM típica de servos
self.kit.frequency = freq
# Posiciones iniciales
self.pan = 90.0
self.tilt = 90.0
self.vx_prev = 0.0
self.vy_prev = 0.0
self.arm()
def arm(self):
self.set_angles(90.0, 90.0)
time.sleep(0.3)
def clamp(self, val, vmin, vmax):
return max(vmin, min(vmax, val))
def set_angles(self, pan_deg, tilt_deg):
self.pan = self.clamp(pan_deg, PAN_MIN, PAN_MAX)
self.tilt = self.clamp(tilt_deg, TILT_MIN, TILT_MAX)
self.kit.servo[PAN_CH].angle = self.pan
self.kit.servo[TILT_CH].angle = self.tilt
def update_from_error(self, err_x_norm, err_y_norm, dt):
# err_x_norm, err_y_norm en [-1, 1] (positivo = target a la derecha/abajo)
# Control proporcional (y derivativo opcional por suavizado)
vx = Kp_pan * err_x_norm
vy = Kp_tilt * err_y_norm
# Derivativo simple
dvx = (vx - self.vx_prev)/dt if dt > 0 else 0.0
dvy = (vy - self.vy_prev)/dt if dt > 0 else 0.0
self.vx_prev, self.vy_prev = vx, vy
pan_cmd = self.pan - (vx + Kd_pan*dvx) # signo negativo: objeto a la derecha => pan aumenta (gira a derecha)
tilt_cmd = self.tilt + (vy + Kd_tilt*dvy) # objeto abajo => aumentar tilt (apunta abajo)
self.set_angles(pan_cmd, tilt_cmd)
class DeepStreamPTZApp:
def __init__(self, sensor_id=0, pgie_config="pgie_resnet10_config.txt",
tracker_config="tracker_iou_config.yml", display=True):
self.sensor_id = sensor_id
self.pgie_config = pgie_config
self.tracker_config = tracker_config
self.display = display
self.pipeline = None
self.bus = None
self.loop = None
self.last_ts = time.time()
self.ptz = PTZController()
self.fps_counter = 0
self.fps_t0 = time.time()
self.target_class_name = None # opcional: filtrar por clase
self.zoom = 1.0
def make_element(self, factory, name=None):
el = Gst.ElementFactory.make(factory, name)
if not el:
print(f"ERROR: no se pudo crear elemento {factory}", file=sys.stderr)
sys.exit(1)
return el
def build_pipeline(self):
pipeline = Gst.Pipeline()
# Fuente CSI
source = self.make_element("nvarguscamerasrc", "source")
source.set_property("sensor-id", self.sensor_id)
source.set_property("bufapi-version", True)
caps_src = self.make_element("capsfilter", "src_caps")
caps_src.set_property("caps", Gst.Caps.from_string(
f"video/x-raw(memory:NVMM), width={FRAME_W}, height={FRAME_H}, framerate={FPS}/1"))
nvvidconv = self.make_element("nvvideoconvert", "nvvidconv")
caps_nvmm = self.make_element("capsfilter", "caps_nvmm")
caps_nvmm.set_property("caps", Gst.Caps.from_string("video/x-raw(memory:NVMM), format=NV12"))
# streammux (batch=1)
streammux = self.make_element("nvstreammux", "stream-muxer")
streammux.set_property("width", FRAME_W)
streammux.set_property("height", FRAME_H)
streammux.set_property("batch-size", 1)
streammux.set_property("batched-push-timeout", 40000)
pgie = self.make_element("nvinfer", "primary-inference")
pgie.set_property("config-file-path", self.pgie_config)
tracker = self.make_element("nvtracker", "tracker")
tracker.set_property("ll-config-file", os.path.abspath(self.tracker_config))
tracker.set_property("ll-lib-file", "/opt/nvidia/deepstream/deepstream/lib/libnvds_iou.so")
tracker.set_property("enable-batch-process", 1)
tracker.set_property("display-tracking-id", 1)
tracker.set_property("tracker-width", 640)
tracker.set_property("tracker-height", 384)
nvvidconv2 = self.make_element("nvvideoconvert", "nvvidconv2")
nvosd = self.make_element("nvdsosd", "onscreendisplay")
nvosd.set_property("process-mode", 0) # GPU
nvosd.set_property("display-text", 1)
if self.display:
sink = self.make_element("nveglglessink", "eglsink")
sink.set_property("sync", 0)
else:
sink = self.make_element("fakesink", "fakesink")
# Construye el grafo
pipeline.add(source)
pipeline.add(caps_src)
pipeline.add(nvvidconv)
pipeline.add(caps_nvmm)
pipeline.add(streammux)
pipeline.add(pgie)
pipeline.add(tracker)
pipeline.add(nvvidconv2)
pipeline.add(nvosd)
pipeline.add(sink)
if not Gst.Element.link_many(source, caps_src, nvvidconv, caps_nvmm):
print("ERROR: link nvarguscamerasrc->caps->nvvideoconvert->caps_nvmm", file=sys.stderr); sys.exit(1)
# Enlace manual al streammux sink_0
sinkpad = streammux.get_request_pad("sink_0")
if not sinkpad:
print("ERROR: no sinkpad en streammux", file=sys.stderr); sys.exit(1)
srcpad = caps_nvmm.get_static_pad("src")
if not srcpad:
print("ERROR: no srcpad en caps_nvmm", file=sys.stderr); sys.exit(1)
if srcpad.link(sinkpad) != Gst.PadLinkReturn.OK:
print("ERROR: link caps_nvmm->streammux", file=sys.stderr); sys.exit(1)
if not Gst.Element.link_many(streammux, pgie, tracker, nvvidconv2, nvosd, sink):
print("ERROR: link streammux->pgie->tracker->...->sink", file=sys.stderr); sys.exit(1)
# Probe tras PGIE o tras tracker para extraer metadatos
pgie_src_pad = pgie.get_static_pad("src")
if not pgie_src_pad:
print("ERROR: no src pad en pgie", file=sys.stderr); sys.exit(1)
pgie_src_pad.add_probe(Gst.PadProbeType.BUFFER, self.infer_probe, 0)
self.pipeline = pipeline
def infer_probe(self, pad, info, udata):
# Extrae batch_meta, objetos, bboxes y actualiza PTZ
buffer = info.get_buffer()
if not buffer:
return Gst.PadProbeReturn.OK
batch_meta = pyds.gst_buffer_get_nvds_batch_meta(hash(buffer))
l_frame = batch_meta.frame_meta_list
now = time.time()
dt = max(1e-3, now - self.last_ts)
self.last_ts = now
# Estado para zoom sugerido
largest_area = 0.0
target_center = (FRAME_W/2.0, FRAME_H/2.0)
tracked_id = -1
while l_frame is not None:
try:
frame_meta = pyds.NvDsFrameMeta.cast(l_frame.data)
except StopIteration:
break
l_obj = frame_meta.obj_meta_list
# Busca el objeto con mayor área (o filtra por clase si se desea)
while l_obj is not None:
try:
obj_meta = pyds.NvDsObjectMeta.cast(l_obj.data)
except StopIteration:
break
rect_params = obj_meta.rect_params
w = rect_params.width
h = rect_params.height
area = w * h
if area > largest_area:
largest_area = area
cx = rect_params.left + w/2.0
cy = rect_params.top + h/2.0
target_center = (cx, cy)
tracked_id = obj_meta.object_id # válido tras tracker; -1 si no hay
try:
l_obj = l_obj.next
except StopIteration:
break
# Dibuja overlay con datos PTZ/zoom
display_meta = pyds.nvds_acquire_display_meta_from_pool(batch_meta)
txt_params = display_meta.text_params[0]
txt_params.display_text = f"PTZ: pan={self.ptz.pan:5.1f} tilt={self.ptz.tilt:5.1f} | zoom={self.zoom:3.1f}x | ID={tracked_id}"
txt_params.x_offset = 10
txt_params.y_offset = 20
txt_params.font_params.font_name = "Serif"
txt_params.font_params.font_size = 16
txt_params.font_params.font_color.set(1.0, 1.0, 1.0, 1.0)
txt_params.set_bg_clr = 1
txt_params.text_bg_clr.set(0.16, 0.16, 0.16, 0.5)
pyds.nvds_add_display_meta_to_frame(frame_meta, display_meta)
try:
l_frame = l_frame.next
except StopIteration:
break
# Control PTZ basado en error del centroide objetivo más grande
err_x = (target_center[0] - FRAME_W/2.0) / (FRAME_W/2.0) # [-1, 1]
err_y = (target_center[1] - FRAME_H/2.0) / (FRAME_H/2.0)
self.ptz.update_from_error(err_x, err_y, dt)
# Zoom digital sugerido en función del área relativa
area_rel = largest_area / (FRAME_W * FRAME_H)
# Si el objeto es pequeño (<3% del frame), incrementa zoom; si muy grande (>30%), reduce
if area_rel < 0.03:
self.zoom = min(ZOOM_MAX, self.zoom + 0.05)
elif area_rel > 0.30:
self.zoom = max(ZOOM_MIN, self.zoom - 0.05)
# Métrica FPS aproximada
self.fps_counter += 1
if now - self.fps_t0 >= 1.0:
print(f"[PTZ] FPS ~ {self.fps_counter:2d} | err=({err_x:+.3f},{err_y:+.3f}) | area_rel={area_rel:.3f} | pan={self.ptz.pan:.1f} tilt={self.ptz.tilt:.1f} | zoom={self.zoom:.1f}x")
self.fps_counter = 0
self.fps_t0 = now
return Gst.PadProbeReturn.OK
def run(self):
self.build_pipeline()
self.bus = self.pipeline.get_bus()
self.bus.add_signal_watch()
self.bus.connect("message", self.bus_call, None)
self.loop = GObject.MainLoop()
print("Iniciando pipeline...")
self.pipeline.set_state(Gst.State.PLAYING)
try:
self.loop.run()
except KeyboardInterrupt:
pass
finally:
print("Deteniendo pipeline...")
self.pipeline.set_state(Gst.State.NULL)
def bus_call(self, bus, message, loop):
t = message.type
if t == Gst.MessageType.EOS:
print("EOS recibido, saliendo")
self.loop.quit()
elif t == Gst.MessageType.ERROR:
err, dbg = message.parse_error()
print(f"ERROR: {err}, debug: {dbg}", file=sys.stderr)
self.loop.quit()
return True
if __name__ == "__main__":
# Variables de entorno útiles (opcional, si no están configuradas)
ds_root = "/opt/nvidia/deepstream/deepstream"
os.environ.setdefault("GST_PLUGIN_PATH", f"{ds_root}/lib/gst-plugins")
os.environ.setdefault("LD_LIBRARY_PATH", f"{ds_root}/lib:" + os.environ.get("LD_LIBRARY_PATH",""))
pybinds = f"{ds_root}/sources/python"
pypaths = [f"{pybinds}/bindings", f"{pybinds}/apps"]
os.environ["PYTHONPATH"] = ":".join(pypaths + [os.environ.get("PYTHONPATH","")])
sensor_id = 0
if len(sys.argv) > 1:
sensor_id = int(sys.argv[1])
app = DeepStreamPTZApp(sensor_id=sensor_id,
pgie_config="pgie_resnet10_config.txt",
tracker_config="tracker_iou_config.yml",
display=True)
app.run()
Notas clave del código:
– El pipeline usa nvarguscamerasrc (CSI IMX477) → nvvideoconvert → nvstreammux → nvinfer (ResNet10) → nvtracker (IOU) → nvosd → nveglglessink.
– El probe en la salida de nvinfer lee metadatos, selecciona el mayor bbox y calcula el error normalizado para el controlador PTZ.
– PCA9685 se controla con adafruit-circuitpython-servokit; el mapeo de ángulos es directo (0–180°). Ajusta Kp y límites a tu mecánica.
– Zoom digital se calcula como factor y se muestra; opcionalmente puedes implementar cropping dinámico en nvvideoconvert con propiedades src-crop si quieres ver el zoom aplicado en la imagen.
Compilación/flash/ejecución
1) Instala DeepStream 6.2 (si no lo tienes)
– En JetPack 5.1.2 puedes instalarlo vía apt:
sudo apt update
sudo apt install -y deepstream-6.2
Verifica la ruta: /opt/nvidia/deepstream/deepstream
2) Instala bindings de Python para DeepStream
cd /opt/nvidia/deepstream/deepstream/sources/python
sudo apt install -y python3-dev python3-gi python3-gst-1.0
pip3 install -r requirements.txt
# Instala pyds (rueda incluida para DS6.2 en aarch64)
pip3 install ./bindings/pyds-1.1.0-py3-none-any.whl
3) Instala librerías para PCA9685/servos
pip3 install adafruit-circuitpython-servokit adafruit-circuitpython-pca9685 smbus2
4) Crea los archivos del proyecto
mkdir -p ~/ptz-deepstream
cd ~/ptz-deepstream
# Copia aquí pgie_resnet10_config.txt y tracker_iou_config.yml (como arriba)
nano pgie_resnet10_config.txt
nano tracker_iou_config.yml
nano ptz_deepstream_xaviernx.py
chmod +x ptz_deepstream_xaviernx.py
5) Prueba cámara con GStreamer (opcional, ya lo hiciste)
gst-launch-1.0 nvarguscamerasrc sensor-id=0 ! \
'video/x-raw(memory:NVMM),width=1920,height=1080,framerate=30/1' ! \
nvvideoconvert ! fpsdisplaysink video-sink=fakesink text-overlay=true sync=false
6) Ajusta potencia y clocks para benchmarks
sudo nvpmodel -m 0
sudo jetson_clocks
7) Ejecuta la app PTZ
cd ~/ptz-deepstream
# Terminal 1: tegrastats para medir recursos
sudo tegrastats
# Terminal 2: ejecuta la app (sensor-id 0 por defecto)
./ptz_deepstream_xaviernx.py 0
Salida esperada (consola):
– Líneas tipo “[PTZ] FPS ~ 30 | err=(+0.012,-0.035) | area_rel=0.045 | pan=… tilt=… | zoom=1.2x”
– Una ventana con el vídeo, bounding boxes y overlay de PTZ/zoom.
– La cámara montada en el soporte pan-tilt girará e inclinará para mantener el objeto más grande centrado.
Para detener: Ctrl+C en el terminal de la app. Revertir clocks:
# Opcional: volver a modo de bajo consumo y clocks por defecto
sudo nvpmodel -m 1
sudo jetson_clocks --restore
Validación paso a paso
1) Verifica hardware I2C
– i2cdetect -y -r 1 debe mostrar 0x40.
– Si no aparece, revisa cableado SDA/SCL, VCC (3.3 V), GND común.
2) Verifica cámara IMX477
– gst-launch-1.0 con nvarguscamerasrc debe entregar 1080p@30.
– Si falla, revisa el cable CSI, sensor-id (0 u 1), y que el driver Arducam para IMX477 esté instalado.
3) Ejecuta la app
– Observa los logs “[PTZ] FPS ~ N”.
– Métrica: N≥25 sostenido; objetivo ≥30 en MAXN.
– En tegrastats:
– GPU util: 20–60% típico; EMC y RAM dentro de márgenes.
– CPU: <30% promedio.
– Observa el movimiento de servos:
– Al mover una persona/objeto frente a la cámara, el soporte debe girar/elevar la cámara para mantenerlo centrado.
– Métrica: retardo percibido <200 ms (puedes medir con un LED o aplauso visible/sonoro y correlacionar logs y vídeo).
4) Comprobación de centrado
– Coloca un marcador en el centro del frame (p.ej., un post-it en la escena).
– Mueve el objeto; el sistema debe converger a ≤ ±3% del ancho/alto del frame (visualmente y por err=(x,y) en logs).
5) Prueba de zoom digital
– Al alejar el objeto (área_rel < 0.03), observa en overlay cómo zoom≈1.5–2.0x.
– Al acercarlo (área_rel > 0.3), regresa a 1.0x. Si quieres ver zoom aplicado, ver Mejores/variantes.
6) Estabilidad térmica
– Ejecuta 5–10 minutos; no debe haber throttling (temperatura sostenida, FPS estable). Si cae FPS, mejora ventilación.
Troubleshooting (errores típicos y solución)
1) Error: “ERROR: no se pudo crear elemento nvarguscamerasrc”
– Causa: GStreamer o plugins no instalados correctamente, o la cámara no está disponible.
– Solución: reinstala gstreamer1.0-plugins-bad; verifica nvargus-daemon; reinicia: sudo systemctl restart nvargus-daemon
2) Cámara IMX477 sin imagen / nvargus error
– Causa: driver del IMX477 no instalado o conflicto de sensor-mode.
– Solución: reinstala/lleva el driver Arducam correspondiente a JetPack 5.1.2; prueba sensor-id=1; comprueba con v4l2-ctl –list-formats-ext -d /dev/video0 (si existe).
3) pyds import error
– Causa: bindings no instalados o PYTHONPATH incorrecto.
– Solución: pip3 install pyds-1.1.0… desde /opt/nvidia/deepstream/deepstream/sources/python/bindings; exporta PYTHONPATH como en el script.
4) No se mueven los servos pese a logs correctos
– Causa: fuente de 5–6 V insuficiente o GND no común.
– Solución: usa fuente dedicada ≥2 A; une GND del Jetson con GND del PCA9685; verifica 0x40 con i2cdetect.
5) Servos vibran o oscilan (hunting)
– Causa: Kp muy alto; holgura mecánica; D inestable.
– Solución: reduce Kp_pan/Kp_tilt; añade Kd (derivativo) pequeño; aplica deadband: si |err|<0.02, no mover.
6) DeepStream pipeline “link error” con streammux
– Causa: falta caps memory:NVMM o pads incorrectos.
– Solución: asegúrate de video/x-raw(memory:NVMM), format=NV12 antes de streammux; revisa la sección build_pipeline.
7) FPS bajo (<15)
– Causa: ventana de visualización en remoto, sink sync, clocks limitados.
– Solución: usa nveglglessink en local; sink.set_property(«sync»,0); activa nvpmodel -m 0 y jetson_clocks; baja resolución a 1280×720.
8) DeepStream “parse-bbox-func-name” incompatible
– Causa: usar una config de parser no acorde al modelo.
– Solución: con ResNet10 usa NvDsInferParseCustomResnet y libnvds_infercustomparser.so (como en el config). Si cambias a YOLO, debes usar parser de YOLO específico.
Mejoras/variantes
- Zoom digital real en pipeline:
- Actualiza dinámicamente propiedades de nvvideoconvert (src-crop) según bounding box para recortar y reenmarcar; o usa nvdspreprocess + nvvideoconvert con caps renegotiation. Alternativamente, crea rama tee: rama A para inferencia full-frame, rama B para visualización con crop dinámico.
- Filtro de clase/ID:
- Filtra por “Person” (label) y/o por tracking ID persistente; usa nvtracker para seleccionar el ID más consistente en lugar del mayor bbox.
- Control avanzado PTZ:
- PID completo con anti-windup; feedforward con estimación de velocidad de objeto; curva de aceleración para suavizado.
- Modelo más robusto:
- Cambia a PeopleNet o YOLOv8 ONNX con nvinfer (requiere parser YOLO) para mejorar detección de personas/objetos en escenas complejas.
- Telemetría:
- Exporta métricas (FPS, error, setpoints) por MQTT/InfluxDB; integra un dashboard (Grafana).
- Ahorro energético:
- Reduce FPS en horario nocturno; apaga servos cuando error
3 s; usa nvpmodel menos exigente.
Checklist de verificación
- [ ] SO y toolchain correctos: JetPack 5.1.2 (L4T 35.4.1), DeepStream 6.2, TensorRT 8.5.2, CUDA 11.4.
- [ ] Cámara Arducam IMX477 entrega 1080p@30 vía nvarguscamerasrc.
- [ ] PCA9685 detectado en i2c-1 (0x40) y servos alimentados con fuente separada; GND común.
- [ ] App Python arranca sin errores; overlay muestra pan/tilt/zoom/ID.
- [ ] FPS ≥ 25 en MAXN; tegrastats muestra GPU 20–60% y CPU <30%.
- [ ] El soporte pan-tilt centra el objeto objetivo con retardo <200 ms y error <±3% del frame.
- [ ] Zoom digital aumenta al alejar el objeto y disminuye al acercarlo; overlay refleja factor.
- [ ] Sistema estable ≥5 minutos sin throttling (sin caída de FPS).
Explicaciones breves de partes clave
- DeepStream nvinfer (ResNet10): usamos el modelo de detector primario incluido para evitar dependencias externas. Se ejecuta en FP16 (network-mode=1) para aprovechar la GPU del Xavier NX.
- nvtracker IOU: asocia detecciones cuadro a cuadro con una métrica IOU simple; suficiente para mantener IDs y evitar saltos violentos del objetivo.
- Probe pyds: engancha la ruta de datos tras PGIE para leer metadatos sin copiar frames; baja latencia.
- Control PTZ con PCA9685: la librería Servokit abstrae el PWM; un simple P controlador convierte error de imagen a ángulos. Ajustar Kp y límites a la cinemática de tu bracket.
- Métricas: el bucle imprime FPS aproximado, error normalizado y área relativa; tegrastats complementa con carga de GPU/CPU y memorias.
Apéndice: comandos útiles y limpieza
- Medición de rendimiento con sink “fakesink”
GST_DEBUG=2 ./ptz_deepstream_xaviernx.py 0
# Cambia display=False en el constructor para usar fakesink si necesitas medir máxima inferencia.
- Limpieza de entorno de clocks
sudo nvpmodel -m 1
sudo jetson_clocks --restore
- Verificación de cargas DeepStream
deepstream-app --version-all
# Comprueba versiones de nvinfer, nvtracker, etc.
Con esto, dispones de un caso práctico completo y reproducible para “ptz-object-tracking” usando exactamente “Jetson Xavier NX Developer Kit + Arducam IMX477 + PCA9685 PWM Driver”, con toolchain y versiones fijadas, pipeline DeepStream acelerado por GPU, control PT basado en I2C y validación cuantitativa.
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.




