Caso práctico: seguimiento PTZ Jetson Xavier NX, IMX477

Caso práctico: seguimiento PTZ Jetson Xavier NX, IMX477 — hero

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 error3 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

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 PTZ que se va a construir?




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




Pregunta 3: ¿Cuál es la latencia máxima permitida para el seguimiento de objetos?




Pregunta 4: ¿Qué tecnología se utilizará para la inferencia en el sistema?




Pregunta 5: ¿Qué tasa de fotogramas por segundo (FPS) se espera alcanzar?




Pregunta 6: ¿Qué tipo de control se usará para los servos?




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




Pregunta 8: ¿Qué componente se utilizará para controlar los servos mediante I2C?




Pregunta 9: ¿Cuál es la precisión de centrado requerida para el sistema?




Pregunta 10: ¿Qué tipo de seguimiento se implementará en el sistema PTZ?




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 to Top