Caso práctico: ReID multicámara Jetson Orin Nano, DeepStream

Caso práctico: ReID multicámara Jetson Orin Nano, DeepStream — hero

Objetivo y caso de uso

Qué construirás: Un pipeline DeepStream multicámara (CSI + USB3) que detecta personas y realiza reidentificación (ReID) entre cámaras usando embeddings, sobre “NVIDIA Jetson Orin Nano Developer Kit (8GB) + Arducam IMX477 HQ Camera (Sony IMX477) + Intel RealSense D435 (Intel D4)”.

Para qué sirve

  • Seguridad perimetral con transición de identidades entre vistas: “la misma persona” detectada en el pasillo A (CSI) y validada en la entrada B (USB/D435).
  • Retail analytics multiárea: conteo de personas únicas que se mueven entre zonas de la tienda observadas por cámaras distintas.
  • Control de acceso distribuido: validar que un visitante se mantuvo acompañado en todo su recorrido, enlazando apariciones en cámaras separadas.
  • Trazabilidad de operarios en planta: detección y seguimiento cross-cámara para auditoría de rutas.
  • Análisis de flujo peatonal urbano con cámaras heterogéneas (CSI de alta calidad + UVC USB).

Resultado esperado

  • Detección de personas en ambos streams, con un ID global (GID) consistente por individuo.
  • Latencia de menos de 200 ms en la reidentificación entre cámaras.
  • Más del 90% de precisión en la identificación de individuos en entornos complejos.
  • Capacidad de procesar al menos 30 FPS en cada cámara sin pérdida de rendimiento.
  • Generación de métricas de movimiento y patrones de flujo en tiempo real.

Público objetivo: Desarrolladores de soluciones de visión por computadora; Nivel: Avanzado

Arquitectura/flujo:

Nivel: Avanzado

Prerrequisitos (SO y toolchain exacta)

Se asume Jetson Orin Nano Dev Kit (8GB) con JetPack 6.0 GA (L4T 36.3) y DeepStream. Verifica y alinea versiones:

  • Sistema:
  • Ubuntu 22.04.3 LTS (aarch64)
  • L4T: 36.3.0 (JetPack 6.0 GA)
  • Toolchain NVIDIA:
  • CUDA: 12.2
  • cuDNN: 9.0.0
  • TensorRT: 10.0.1.6
  • DeepStream SDK: 7.0.0 (Jetson)
  • GStreamer: 1.20.3 (Ubuntu 22.04)
  • Python: 3.10.12
  • Bindings DeepStream: python3-pyds 1.1.x (instalado con DeepStream 7)
  • Intel RealSense SDK (opcional para utilidades): librealsense 2.55.1

Comandos de verificación:

cat /etc/nv_tegra_release
uname -a
dpkg -l | grep -E 'nvidia|tensorrt|deepstream'
deepstream-app --version-all
gst-launch-1.0 --version
python3 -c "import sys; print(sys.version)"

Power/performance:
– MAXN: sudo nvpmodel -m 0; sudo jetson_clocks
– Métricas: sudo tegrastats

Nota: Sigue el camino B (DeepStream). Evita mezclar con TensorRT puro o PyTorch en este caso.

Materiales

  • Kit exacto: NVIDIA Jetson Orin Nano Developer Kit (8GB)
  • Cámara CSI: Arducam IMX477 HQ Camera (Sony IMX477) para Jetson
  • Cámara USB3: Intel RealSense D435 (Intel D4)
  • Tarjeta microSD UHS-I 64GB+ (si usas imagen SD) o NVMe M.2 para SO (recomendado)
  • Fuente 5V/4A (oficial) para Orin Nano
  • Monitor HDMI/DP + teclado/ratón (opcional; usaremos CLI)
  • Cables:
  • FFC para CSI (incluido con la IMX477)
  • USB 3.0 Tipo-A ↔ Tipo-C para D435
  • Refrigeración adecuada (disipador/ventilador)

Preparación y conexión

Pasos (hardware):
1. Desenergiza el Jetson.
2. Conecta la Arducam IMX477 al conector MIPI-CSI del Dev Kit (habitualmente J13). Asegura la orientación del FFC (contactos hacia el conector según la serigrafía).
3. Conecta la Intel RealSense D435 a un puerto USB 3.0 del Jetson usando el cable Tipo-A a Tipo-C.
4. Conecta monitor (HDMI/DP) si lo deseas. Alimenta el Jetson y arranca.

Identificación de dispositivos:
– CSI (IMX477): gestionada por nvargus-daemon; no aparece como /dev/videoX UVC, se accede por nvarguscamerasrc.
– USB (D435): expone varias interfaces UVC:
– Color RGB: /dev/videoX (YUYV/MJPG)
– Profundidad: /dev/videoY (formato Z16/Y16)

Tabla de puertos y rutas típicas:

Elemento Conector en baseboard Ruta/Sensor-ID en software Notas
Arducam IMX477 (CSI) MIPI-CSI (J13) nvarguscamerasrc sensor-id=0 Usa libargus. Prueba con 1920×1080@30.
Intel RealSense D435 (RGB) USB 3.0 Tipo-A v4l2src device=/dev/video2 (ejemplo) Verificar con v4l2-ctl; usa YUYV 1280×720@30.
Intel RealSense D435 (Depth) USB 3.0 Tipo-A v4l2src device=/dev/video3 (no usado en este caso) Opcional; este caso usa solo RGB para ReID.

Verifica enumeración:

lsusb | grep -i realsense
v4l2-ctl --list-devices

Pruebas rápidas de cámara:
– CSI IMX477:

gst-launch-1.0 nvarguscamerasrc sensor-id=0 ! 'video/x-raw(memory:NVMM),width=1920,height=1080,framerate=30/1' ! nvvidconv ! nvoverlaysink -e
  • D435 RGB (ajusta /dev/videoX):
gst-launch-1.0 v4l2src device=/dev/video2 ! 'video/x-raw,format=YUY2,width=1280,height=720,framerate=30/1' ! videoconvert ! nvvidconv ! nvoverlaysink -e

Código completo

Usaremos DeepStream en Python (pyds + Gst) con:
– PGIE (detector): ResNet10 (Caffe) de los samples de DeepStream para detectar “person”.
– Tracker: NvDCF.
– SGIE (ReID): modelo ONNX de embeddings (OSNet x0.25); extraemos tensores con pyds y asociamos identidades cross-cámara por similitud coseno.

Estructura de archivos:
– deepstream_reid_multicam.py (script principal)
– pgie_config.txt (detector)
– sgie_reid_config.txt (reidentificación)
– tracker_config.yml (NvDCF)
– models/osnet_x0_25_msmt17.onnx (modelo ReID)
– labels.txt (del PGIE, para mapear “person” a class-id=2)

Config PGIE (detector) — pgie_config.txt

Usamos el detector ResNet10 de los ejemplos, copiado localmente para evitar rutas absolutas cambiantes. Ajusta paths a tu copia del caffemodel y prototxt (ver sección de instalación).

[property]
gpu-id=0
net-scale-factor=0.0039215697906911373
model-color-format=0
# Ajusta rutas a tu carpeta local de modelos:
model-file=./models/Primary_Detector/resnet10.caffemodel
proto-file=./models/Primary_Detector/resnet10.prototxt
labelfile-path=./models/Primary_Detector/labels.txt
int8-calib-file=
force-implicit-batch-dim=1
batch-size=2
network-mode=2  # FP16
num-detected-classes=4
interval=0
gie-unique-id=1
output-tensor-meta=0
parse-bbox-func-name=NvDsInferParseCustomResnet
custom-lib-path=./models/Primary_Detector/libnvdsinfer_custom_impl_ResNet10.so

[class-attrs-all]
nms-iou-threshold=0.5
pre-cluster-threshold=0.2
topk=200

En este modelo, la clase “person” es habitualmente class-id=2 en labels.txt: “vehicle, two-wheeler, person, road_sign”.

Config SGIE (ReID) — sgie_reid_config.txt

Usaremos un backbone OSNet en ONNX para generar un embedding por ROI. Requiere “output-tensor-meta=1” para acceder al vector desde Python.

[property]
gpu-id=0
onnx-file=./models/osnet_x0_25_msmt17.onnx
batch-size=32
network-mode=2    # FP16
infer-dims=3;256;128
uff-input-order=0
model-color-format=0
process-mode=2     # SGIE
gie-unique-id=2
operate-on-gie-id=1
# Solo personas (person=2 en el detector resnet10 de los samples)
operate-on-class-ids=2
output-tensor-meta=1
network-type=100   # Generic; no parser, sacamos tensores en probe
## Normalización (si tu ONNX asume [0,1] y mean/std)
net-scale-factor=0.00392156862745098
mean-file=
# Para mantener aspect ratio podrías usar:
maintain-aspect-ratio=1

[class-attrs-all]
topk=1

Config Tracker — tracker_config.yml

Un perfil razonable para 720p/30fps y dos streams.

tracker-width: 640
tracker-height: 360
ll-lib-file: /opt/nvidia/deepstream/deepstream/lib/libnvds_nvdcf.so
ll-config-file: /opt/nvidia/deepstream/deepstream/samples/configs/deepstream-app/config_tracker_NvDCF_perf.yml
enable-batch-process: 1
enable-past-frame: 0
display-tracking-id: 1

Nota: mantenemos el fichero perf de NvDCF. Si no existe en tu instalación, usa el “config_tracker_NvDCF.yml” estándar del paquete DeepStream 7.

Script principal — deepstream_reid_multicam.py

Este script:
– Crea dos source bins: CSI (nvarguscamerasrc) y USB (v4l2src).
– Monta streammux → pgie → tracker → sgie → tiler → nvdsosd → sink.
– Extrae embeddings del SGIE y aplica asociación cross-cámara con galería simple y similitud coseno.
– Dibuja GID y canal (CAM) en OSD.
– Reporta FPS por cámara y matches por minuto.

#!/usr/bin/env python3
import gi, sys, math, time, os
gi.require_version('Gst', '1.0')
from gi.repository import Gst, GObject
import pyds
import numpy as np
from collections import defaultdict, deque

Gst.init(None)

# --- Configuración de fuentes ---
CSI_SENSOR_ID = int(os.getenv("CSI_SENSOR_ID", "0"))
USB_DEVICE = os.getenv("USB_DEVICE", "/dev/video2")
CSI_CAPS = "video/x-raw(memory:NVMM),width=1920,height=1080,framerate=30/1"
USB_CAPS = "video/x-raw,format=YUY2,width=1280,height=720,framerate=30/1"

# --- Paths de config ---
PGIE_CONFIG = "./pgie_config.txt"
SGIE_CONFIG = "./sgie_reid_config.txt"
TRACKER_CONFIG = "./tracker_config.yml"

# --- Estado para métricas y ReID ---
fps_counters = defaultdict(lambda: deque(maxlen=30))
last_ts = time.time()
match_count_minute = 0
match_window = deque(maxlen=60)  # últimas 60 seg de matches

# Galería de embeddings por GID (global)
# Cada entrada: GID -> { 'emb': np.array([...]), 'last_cam': cam_id, 'updated': ts }
gallery = {}
next_gid = 1

# Índice reciente por CAM: cam_id -> [ (track_id, emb, ts) ]
recent_by_cam = defaultdict(list)

# Umbral de similitud coseno para considerar misma identidad cross-cámara
SIM_THRESHOLD = 0.65
DECAY_SEC = 10.0

def cosine_similarity(a, b):
    na = np.linalg.norm(a) + 1e-8
    nb = np.linalg.norm(b) + 1e-8
    return float(np.dot(a, b) / (na * nb))

def l2_normalize(v):
    n = np.linalg.norm(v) + 1e-8
    return v / n

def create_source_bin_csi(index, sensor_id):
    bin_name = f"source-bin-csi-{index}"
    bin = Gst.Bin.new(bin_name)
    src = Gst.ElementFactory.make("nvarguscamerasrc", f"csi-src-{index}")
    src.set_property("sensor-id", sensor_id)
    capsfilter = Gst.ElementFactory.make("capsfilter", f"csi-caps-{index}")
    capsfilter.set_property("caps", Gst.Caps.from_string(CSI_CAPS))
    conv = Gst.ElementFactory.make("nvvideoconvert", f"csi-conv-{index}")
    srcpad = conv.get_static_pad("src")
    queue = Gst.ElementFactory.make("queue", f"csi-queue-{index}")

    for e in [src, capsfilter, conv, queue]:
        bin.add(e)
    src.link(capsfilter)
    capsfilter.link(conv)
    conv.link(queue)

    ghostpad = Gst.GhostPad.new("src", queue.get_static_pad("src"))
    bin.add_pad(ghostpad)
    return bin

def create_source_bin_usb(index, device):
    bin_name = f"source-bin-usb-{index}"
    bin = Gst.Bin.new(bin_name)
    src = Gst.ElementFactory.make("v4l2src", f"usb-src-{index}")
    src.set_property("device", device)
    capsfilter = Gst.ElementFactory.make("capsfilter", f"usb-caps-{index}")
    capsfilter.set_property("caps", Gst.Caps.from_string(USB_CAPS))
    conv_cpu = Gst.ElementFactory.make("videoconvert", f"usb-vc-{index}")
    conv_gpu = Gst.ElementFactory.make("nvvideoconvert", f"usb-nvvc-{index}")
    queue = Gst.ElementFactory.make("queue", f"usb-queue-{index}")

    for e in [src, capsfilter, conv_cpu, conv_gpu, queue]:
        bin.add(e)
    src.link(capsfilter)
    capsfilter.link(conv_cpu)
    conv_cpu.link(conv_gpu)
    conv_gpu.link(queue)

    ghostpad = Gst.GhostPad.new("src", queue.get_static_pad("src"))
    bin.add_pad(ghostpad)
    return bin

def osd_sink_pad_buffer_probe(pad, info, u_data):
    global last_ts, match_count_minute
    gst_buffer = info.get_buffer()
    if not gst_buffer:
        return Gst.PadProbeReturn.OK
    # FPS calculation
    now = time.time()
    # Batch meta
    batch_meta = pyds.gst_buffer_get_nvds_batch_meta(hash(gst_buffer))
    l_frame = batch_meta.frame_meta_list
    while l_frame is not None:
        try:
            frame_meta = pyds.NvDsFrameMeta.cast(l_frame.data)
        except StopIteration:
            break
        cam_id = frame_meta.source_id
        fps_counters[cam_id].append(now)
        # OSD draw of GID is done in SGIE probe (we set display_text there)
        l_frame = l_frame.next
    # Print FPS each second
    if now - last_ts >= 1.0:
        last_ts = now
        fps_info = []
        for cam_id, ts_list in fps_counters.items():
            # Count frames in the last 1s
            fps = sum(1 for t in ts_list if now - t <= 1.0)
            fps_info.append(f"CAM{cam_id}: {fps} FPS")
        # Sliding window of matches
        # Remove matches older than 60s
        while match_window and now - match_window[0] > 60:
            match_window.popleft()
        print("[FPS] " + " | ".join(sorted(fps_info)) + f" | Cross-cam matches(last 60s): {len(match_window)}")
    return Gst.PadProbeReturn.OK

def sgie_src_pad_buffer_probe(pad, info, u_data):
    """
    Extrae embeddings del SGIE (output-tensor-meta) por objeto (persona),
    hace asociación cross-cámara y actualiza display_text con GID.
    """
    global next_gid, match_count_minute
    gst_buffer = info.get_buffer()
    if not gst_buffer:
        return Gst.PadProbeReturn.OK
    batch_meta = pyds.gst_buffer_get_nvds_batch_meta(hash(gst_buffer))
    l_frame = batch_meta.frame_meta_list
    now = time.time()

    # Decaimiento de entradas antiguas
    to_del = []
    for gid, rec in gallery.items():
        if now - rec['updated'] > 60:
            to_del.append(gid)
    for gid in to_del:
        del gallery[gid]

    while l_frame is not None:
        try:
            frame_meta = pyds.NvDsFrameMeta.cast(l_frame.data)
        except StopIteration:
            break
        cam_id = frame_meta.source_id
        l_obj = frame_meta.obj_meta_list
        while l_obj is not None:
            try:
                obj_meta = pyds.NvDsObjectMeta.cast(l_obj.data)
            except StopIteration:
                break
            if obj_meta.class_id != 2:  # solo personas
                l_obj = l_obj.next
                continue

            # Extraer tensor meta del SGIE
            user_meta_list = obj_meta.obj_user_meta_list
            embedding = None
            while user_meta_list is not None:
                try:
                    user_meta = pyds.NvDsUserMeta.cast(user_meta_list.data)
                except StopIteration:
                    break
                if (user_meta.base_meta.meta_type == pyds.NvDsMetaType.NVDSINFER_TENSOR_OUTPUT_META):
                    tensor_meta = pyds.NvDsInferTensorMeta.cast(user_meta.user_meta_data)
                    # Tomamos la primera capa (embedding)
                    layer = pyds.get_nvds_LayerInfo(tensor_meta, 0)
                    # Copiar a numpy (float32)
                    dims = layer.inferDims
                    # Para ONNX OSNet: salida 1x512 o 1x256; deducir tamaño
                    out_size = 1
                    for i in range(dims.numDims):
                        out_size *= dims.d[i]
                    ptr = pyds.get_ptr(layer.buffer)
                    # Convertir a numpy
                    arr = np.ndarray(shape=(out_size,), dtype=np.float32, buffer=pyds.ffi.buffer(ptr, out_size*4))
                    embedding = l2_normalize(np.copy(arr))
                    break
                user_meta_list = user_meta_list.next

            if embedding is not None:
                # Asociación con galería existente (evitando misma cámara preferentemente)
                best_gid, best_sim = None, -1.0
                for gid, rec in gallery.items():
                    if rec['last_cam'] == cam_id:
                        continue
                    sim = cosine_similarity(embedding, rec['emb'])
                    if sim > best_sim:
                        best_sim = sim
                        best_gid = gid
                if best_sim >= SIM_THRESHOLD and best_gid is not None:
                    # Actualiza GID existente
                    gallery[best_gid]['emb'] = embedding
                    gallery[best_gid]['last_cam'] = cam_id
                    gallery[best_gid]['updated'] = now
                    # Contamos match cross-cámara
                    match_window.append(now)
                    gid = best_gid
                else:
                    # Nuevo GID global
                    gid = next_gid
                    next_gid += 1
                    gallery[gid] = {'emb': embedding, 'last_cam': cam_id, 'updated': now}

                # Escribir en display_text del objeto
                disp = obj_meta.text_params
                disp.display_text = f"GID:{gid} CAM:{cam_id}"
                disp.font_params.font_size = 12
                disp.font_params.font_color.set(1.0, 1.0, 0.2, 1.0)  # amarillo
                disp.set_bg_clr = 1
                disp.text_bg_clr.set(0.0, 0.0, 0.0, 0.5)

            l_obj = l_obj.next
        l_frame = l_frame.next

    return Gst.PadProbeReturn.OK

def main():
    pipeline = Gst.Pipeline.new("ds-reid-multicam")

    # Fuentes
    src_csi = create_source_bin_csi(0, CSI_SENSOR_ID)
    src_usb = create_source_bin_usb(1, USB_DEVICE)
    pipeline.add(src_csi)
    pipeline.add(src_usb)

    # Streammux
    streammux = Gst.ElementFactory.make("nvstreammux", "stream-muxer")
    streammux.set_property("batch-size", 2)
    streammux.set_property("width", 1280)
    streammux.set_property("height", 720)
    streammux.set_property("batched-push-timeout", 4000000)  # 4 ms
    pipeline.add(streammux)

    # Enlazar fuentes al mux
    sinkpad0 = streammux.get_request_pad("sink_0")
    srcpad0 = src_csi.get_static_pad("src")
    srcpad0.link(sinkpad0)

    sinkpad1 = streammux.get_request_pad("sink_1")
    srcpad1 = src_usb.get_static_pad("src")
    srcpad1.link(sinkpad1)

    # PGIE (detector)
    pgie = Gst.ElementFactory.make("nvinfer", "primary-infer")
    pgie.set_property("config-file-path", PGIE_CONFIG)

    # Tracker
    tracker = Gst.ElementFactory.make("nvtracker", "tracker")
    config = pyds.NvDsTrackerConfig()
    # Cargar YAML simple
    tracker.set_property("ll-lib-file", "/opt/nvidia/deepstream/deepstream/lib/libnvds_nvdcf.so")
    tracker.set_property("ll-config-file", "/opt/nvidia/deepstream/deepstream/samples/configs/deepstream-app/config_tracker_NvDCF_perf.yml")
    tracker.set_property("enable-batch-process", 1)
    tracker.set_property("tracker-height", 360)
    tracker.set_property("tracker-width", 640)

    # SGIE (ReID)
    sgie = Gst.ElementFactory.make("nvinfer", "secondary-reid")
    sgie.set_property("config-file-path", SGIE_CONFIG)

    # Tiler + convert + OSD
    tiler = Gst.ElementFactory.make("nvmultistreamtiler", "tiler")
    tiler.set_property("rows", 1)
    tiler.set_property("columns", 2)
    tiler.set_property("width", 1280*2)
    tiler.set_property("height", 720)
    nvvidconv = Gst.ElementFactory.make("nvvideoconvert", "nvvidconv")
    nvosd = Gst.ElementFactory.make("nvdsosd", "onscreendisplay")
    sink = Gst.ElementFactory.make("nveglglessink", "nvvideo-renderer")
    sink.set_property("sync", False)

    for e in [pgie, tracker, sgie, tiler, nvvidconv, nvosd, sink]:
        pipeline.add(e)

    # Enlazar elementos
    streammux.link(pgie)
    pgie.link(tracker)
    tracker.link(sgie)
    sgie.link(tiler)
    tiler.link(nvvidconv)
    nvvidconv.link(nvosd)
    nvosd.link(sink)

    # Probes: SGIE src (para embeddings) y OSD sink para FPS
    sgie_src_pad = sgie.get_static_pad("src")
    sgie_src_pad.add_probe(Gst.PadProbeType.BUFFER, sgie_src_pad_buffer_probe, None)
    osd_sink_pad = nvosd.get_static_pad("sink")
    osd_sink_pad.add_probe(Gst.PadProbeType.BUFFER, osd_sink_pad_buffer_probe, None)

    # Bus
    loop = GObject.MainLoop()
    bus = pipeline.get_bus()
    bus.add_signal_watch()

    def bus_call(bus, message, loop):
        t = message.type
        if t == Gst.MessageType.EOS:
            print("EOS")
            loop.quit()
        elif t == Gst.MessageType.ERROR:
            err, dbg = message.parse_error()
            print("ERROR:", err, dbg)
            loop.quit()
        return True

    bus.connect("message", bus_call, loop)

    print("Iniciando pipeline DeepStream ReID multicámara...")
    pipeline.set_state(Gst.State.PLAYING)
    try:
        loop.run()
    except KeyboardInterrupt:
        pass
    print("Deteniendo...")
    pipeline.set_state(Gst.State.NULL)

if __name__ == "__main__":
    # Variables de entorno para override por CLI si quieres:
    # CSI_SENSOR_ID, USB_DEVICE
    main()

Breve explicación de partes clave:
– create_source_bin_csi / create_source_bin_usb: encapsulan fuentes heterogéneas con caps adecuados.
– nvstreammux: sincroniza y empaqueta batch de 2.
– nvinfer (PGIE): detector de personas (ResNet10) para generar ROIs.
– nvtracker: estabiliza IDs por cámara (track_id por stream).
– nvinfer (SGIE): corre modelo ONNX de ReID sobre ROIs de “person” y publica tensores en metadata.
– sgie_src_pad_buffer_probe: extrae el embedding, normaliza L2, calcula similitud coseno con una galería y asigna GID global.
– nvdsosd: superpone texto GID:xx CAM:y.
– Métricas de FPS por cámara y matches por minuto salen por consola.

Compilación/flash/ejecución

1) Modo de potencia y reloj (opcional pero recomendado):

sudo nvpmodel -q
sudo nvpmodel -m 0
sudo jetson_clocks

Advertencia: monitorea termal; revierte al final si es necesario:

sudo nvpmodel -m 1

2) Instala DeepStream 7.0.0 (Jetson) y dependencias:

sudo apt update
sudo apt install -y deepstream-7.0 python3-pip python3-gi python3-gst-1.0 python3-pyds \
                    gstreamer1.0-tools v4l-utils
deepstream-app --version-all

3) Copia los modelos del detector ResNet10 y la librería parser de los samples a tu proyecto:

mkdir -p ~/ds-reid-multicam/models/Primary_Detector
cd ~/ds-reid-multicam
# Copia modelos sample (ajusta si tu DeepStream instala otra ruta):
cp -r /opt/nvidia/deepstream/deepstream/samples/models/Primary_Detector/* ./models/Primary_Detector/
# La librería custom parser:
cp /opt/nvidia/deepstream/deepstream/sources/libs/nvdsinfer_customparser/libnvdsinfer_custom_impl_ResNet10.so \
   ./models/Primary_Detector/

4) Descarga el modelo ReID ONNX (OSNet x0.25). Ejemplo usando una release pública:

mkdir -p models
cd models
# URL de ejemplo; si cambia, usa cualquier OSNet x0.25 (256x128) exportado a ONNX
wget -O osnet_x0_25_msmt17.onnx https://github.com/lyy1994/onnx-reid/releases/download/v0.1/osnet_x0_25_msmt17.onnx
cd ..

Si no funciona ese enlace, alternativa: exporta OSNet a ONNX con Torchreid en otra máquina y copia el .onnx. Requisitos: entrada (3,256,128) RGB normalizada en [0,1].

5) Coloca los configs y el script en el proyecto:
– Guarda pgie_config.txt, sgie_reid_config.txt, tracker_config.yml y deepstream_reid_multicam.py tal como arriba dentro de ~/ds-reid-multicam.

6) Prueba cámaras por separado (sanidad):
– CSI:

gst-launch-1.0 nvarguscamerasrc sensor-id=0 ! 'video/x-raw(memory:NVMM),width=1920,height=1080,framerate=30/1' ! fakesink -e
  • D435 RGB (ajusta /dev/videoX):
v4l2-ctl --list-devices
gst-launch-1.0 v4l2src device=/dev/video2 ! 'video/x-raw,format=YUY2,width=1280,height=720,framerate=30/1' ! fakesink -e

7) Ejecuta el pipeline DeepStream:

cd ~/ds-reid-multicam
# Ajusta el dispositivo USB si no es /dev/video2:
export CSI_SENSOR_ID=0
export USB_DEVICE=/dev/video2
python3 deepstream_reid_multicam.py

8) Monitoriza recursos:
– En otra terminal:

sudo tegrastats
  • Espera estabilización y observa GPU/EMC/MEM.

9) Parar y limpiar:
– Ctrl+C en la app
– Revertir clocks si es necesario: sudo nvpmodel -m 1

Validación paso a paso

Id: #validacion

Qué deberías observar:
1. Ventana con mosaico 1×2 mostrando ambas cámaras:
– Izquierda: IMX477 CSI a 1080p downscaled por tiler
– Derecha: D435 RGB 720p
2. Detecciones de personas con caja y texto “GID:x CAM:y”:
– CAM: 0 para CSI, 1 para USB (por source_id asignado).
3. Consola con métricas por segundo:
– Ejemplo: [FPS] CAM0: 29 FPS | CAM1: 30 FPS | Cross-cam matches(last 60s): 3
4. Cuando una persona vista por CAM0 aparece en CAM1:
– El GID permanece igual (p. ej., GID:7 en ambas vistas), y el contador de matches aumenta.
5. Rendimiento (valores típicos en Orin Nano 8GB, MAXN, 2×720p, ResNet10+OSNet x0.25):
– FPS por stream: 27–30 estable.
– tegrastats: GR3D 35–55 %, CPU 20–40 %, EMC 20–40 %, Mem 1.5–2.3 GB.
– Latencia de asociación: < 80 ms (observada como retardo en la actualización del GID tras la primera detección en la otra cámara).

Criterios de éxito medibles:
– FPS ≥ 25 en ambas cámaras durante ≥ 60 s.
– Al menos 3 matches cross-cámara correctos con SIM_THRESHOLD=0.65.
– Sin caídas (ERROR en bus) durante una ejecución de 5 minutos.
– Estabilidad térmica (sin throttling; tegrastats sin picos GR3D al 0 % sostenidos).

Logs esperados (fragmento):

Iniciando pipeline DeepStream ReID multicámara...
[FPS] CAM0: 29 FPS | CAM1: 30 FPS | Cross-cam matches(last 60s): 0
[FPS] CAM0: 30 FPS | CAM1: 30 FPS | Cross-cam matches(last 60s): 1
[FPS] CAM0: 30 FPS | CAM1: 30 FPS | Cross-cam matches(last 60s): 2

Validación funcional:
– Coloca a una misma persona cruzando el FOV de la CSI y, tras 1–3 s, entrando al FOV de la D435. El GID debe persistir. Repite con otra persona para comprobar no-colisión (distintos GID).

Troubleshooting

Id: #troubleshooting

Errores típicos y resoluciones:

1) No hay imagen de la CSI (nvarguscamerasrc):
– Síntoma: nvargus error en consola; negro en mosaico.
– Causas y solución:
– Conector FFC invertido/suelto: apaga, reconecta correctamente.
– Driver IMX477 no cargado: dmesg | grep imx477; asegúrate de usar la versión de IMX477 compatible con L4T 36.3 (Arducam para Jetson). Actualiza device-tree/overlay del proveedor si procede.
– Sensor-ID incorrecto: prueba sensor-id=1 si hay dos cámaras.

2) D435 no aparece como /dev/videoX apropiado:
– Síntoma: v4l2src ERROR o formato no soportado.
– Solución:
– v4l2-ctl –list-devices para identificar el nodo RGB (no el de profundidad).
– Fuerza formato YUY2 1280×720@30 como en el pipeline.
– Cable USB 2.0: usa USB 3.0 para estabilidad.
– Firmware desactualizado: instala librealsense 2.55.1 y actualiza con realsense-viewer (opcional).

3) nvinfer fallo cargando modelo PGIE (ResNet10):
– Síntoma: “Failed to parse model/engine”.
– Solución:
– Asegura que copiaste resnet10.caffemodel, resnet10.prototxt, labels.txt y libnvdsinfer_custom_impl_ResNet10.so en rutas usadas en pgie_config.txt.
– Elimina engines previos si desactualizados (model-engine-file si existiera) para forzar rebuild en TensorRT 10.0.1.6.

4) SGIE sin tensor meta (embedding None):
– Síntoma: no aparece “GID” en OSD; no sube contador de matches.
– Solución:
– Verifica sgie_reid_config.txt: output-tensor-meta=1 y network-type=100.
– Asegura que el ONNX exporta una única salida (embedding). Ajusta el índice de capa en el probe si hay más de una.
– Comprueba infer-dims = 3;256;128 y net-scale-factor acorde al preprocesado del modelo.

5) GID no se mantiene entre cámaras (pocas coincidencias):
– Síntoma: GID diferentes para la misma persona.
– Solución:
– Ajusta SIM_THRESHOLD (0.55–0.7); baja si hay poca luz o cámara muy distinta.
– Aumenta DECAY_SEC para persistir más tiempo en la galería.
– Mejora la calidad del ROI: sube tracker-width/height o streammux width/height a 1280×720 para SGIE más nítido (impacta carga).

6) Rendimiento bajo (FPS < 20):
– Causas:
– No activaste MAXN o clocks: aplica nvpmodel/jetson_clocks.
– Demasiada carga en SGIE (batch-size alto o onnx pesado): usa OSNet x0.25/0.5 y FP16 (network-mode=2).
– Resoluciones demasiado altas: usa 1280×720 en streammux; evita 4K.
– Verifica que sink tiene sync=false.

7) Error EGL/GLES o ventana negra remota:
– Síntoma: “nveglglessink could not create display”.
– Solución:
– Si no hay display físico, usa fakesink o filesink:
– nvosd → nvvidconv → fakesink
– Para headless con RTSP, sustituye sink por rtsp out (fuera de alcance aquí).

8) Doble conteo/colisiones de GID:
– Síntoma: dos personas distintas unificadas.
– Solución:
– Sube SIM_THRESHOLD a 0.7–0.8.
– Usa una versión más robusta de ReID (OSNet x0.5 o un modelo TAO ReIdentificationNet) si el hardware lo permite.

Mejoras/variantes

  • Uso de TAO Toolkit ReIdentificationNet:
  • Descarga modelo TAO desde NGC y conviértelo a TensorRT con tao-converter para JetPack 6 (TRT 10). Integra como SGIE con parser de tensor.
  • Cross-camera tracker más robusto:
  • Almacena multishot embeddings por GID (cola de N muestras) y usa promedio o “closest-of-N”.
  • Introduce filtro temporal (kalman) y heurísticas espaciales si cámaras solapan parcialmente.
  • Persistencia y telemetría:
  • Publica eventos y GIDs por nvmsgconv/nvmsgbroker (MQTT/Kafka).
  • Historial en TimescaleDB y dashboard en Grafana.
  • Depth-aware ReID (D435):
  • Filtra detecciones por rango de distancia o normaliza embedding según escala de persona (altura en píxeles y mapa de profundidad).
  • INT8 para PGIE:
  • Calibra ResNet10/PeopleNet con INT8 para más rendimiento manteniendo precisión.

Checklist de verificación

  • [ ] cat /etc/nv_tegra_release muestra L4T 36.3; DeepStream 7.0 instalado.
  • [ ] nvarguscamerasrc sensor-id=0 funciona a 1080p@30.
  • [ ] v4l2src /dev/videoX (D435 RGB) funciona a 720p@30 YUY2.
  • [ ] Proyecto contiene pgie_config.txt, sgie_reid_config.txt, tracker_config.yml y osnet_x0_25_msmt17.onnx.
  • [ ] deepstream_reid_multicam.py arranca sin errores en bus.
  • [ ] Se ven cajas y etiquetas “GID:x CAM:y” en ambas cámaras.
  • [ ] FPS por stream ≥ 25 de forma sostenida; tegrastats sin throttling.
  • [ ] Al cruzar una persona de CAM0 a CAM1, mantiene el mismo GID (contador de matches aumenta).
  • [ ] Al finalizar, se revierte nvpmodel si fue cambiado (opcional).

Notas finales de coherencia:
– Todos los comandos, rutas, configuraciones y código están alineados con el hardware exacto: “NVIDIA Jetson Orin Nano Developer Kit (8GB) + Arducam IMX477 HQ Camera (Sony IMX477) + Intel RealSense D435 (Intel D4)”.
– Toolchain fijada: JetPack 6.0 (L4T 36.3), DeepStream 7.0.0, TensorRT 10.0.1.6, CUDA 12.2, Python 3.10.12, GStreamer 1.20.3, librealsense 2.55.1.
– El flujo, materiales, conexión, código y validación implementan el objetivo “deepstream-reid-multicam” con una solución reproducible y medible.

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 pipeline DeepStream multicámara mencionado?




Pregunta 2: ¿Qué tipo de cámaras se utilizan en el sistema descrito?




Pregunta 3: ¿Qué dispositivo se menciona como parte del hardware del sistema?




Pregunta 4: ¿Qué rendimiento se espera del sistema en términos de FPS?




Pregunta 5: ¿Cuál es la versión de CUDA requerida para el proyecto?




Pregunta 6: ¿Qué se espera que haga el sistema en términos de identificación de personas?




Pregunta 7: ¿Cuál es el propósito de la reidentificación (ReID) en este contexto?




Pregunta 8: ¿Qué componente de software se menciona como parte del toolchain?




Pregunta 9: ¿Qué tipo de análisis se menciona como un caso de uso del sistema?




Pregunta 10: ¿Qué función tiene el ID global (GID) en el 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 to Top