Caso práctico: RTSP difuminado facial Jetson Orin NX+IMX477

Caso práctico: RTSP difuminado facial Jetson Orin NX+IMX477 — hero

Objetivo y caso de uso

Qué construirás: Un servidor RTSP en NVIDIA Jetson Orin NX que captura vídeo de una Arducam USB3.0 (Sony IMX477), detecta caras con IA acelerada por GPU y aplica difuminado en tiempo real para preservar la privacidad. Optimizado para streaming LAN de baja latencia.

Para qué sirve

  • Difuminar caras en CCTV para cumplimiento de RGPD al compartir vídeo con terceros.
  • Supervisión de entornos industriales sin exponer identidades del personal en streamings a salas de control.
  • Transmisión de eventos internos donde se requiere anonimizar asistentes (p. ej., visitas a laboratorio).
  • Pruebas de IA perimetral en entornos de desarrollo con salvaguarda de datos personales.
  • Generar datasets “anónimos” de vídeo para etiquetado y pruebas sin revelar rasgos faciales.

Resultado esperado

  • RTSP accesible en la URL rtsp://IP_DEL_JETSON:8554/faceblur, reproducible con VLC o ffplay.
  • FPS sostenido ≥ 20 FPS a 1280×720; latencia extremo a extremo < 250 ms en LAN cableada.
  • Porcentaje de detección de caras frontalizadas > 90% en interiores bien iluminados, con blur aplicado sobre cada rostro detectado.
  • Utilización de GPU (tegrastats) entre 10% y 35% según escena, número de rostros y condiciones de iluminación.

Público objetivo: Integradores de CCTV, ingenieros de visión/IA, equipos de I+D industrial; Nivel: Intermedio–avanzado (Linux/Jetson, GStreamer/RTSP).

Arquitectura/flujo: Arducam USB3.0 (IMX477) → captura (USB3) → inferencia de detección de rostros en GPU → aplicación de blur en ROIs → codificación H.264 por hardware → servidor RTSP (puerto 8554).

Prerrequisitos (SO y toolchain)

Para coherencia con el modelo y asegurar reproducibilidad, este caso práctico se ha validado con:

  • Dispositivo: NVIDIA Jetson Orin NX Developer Kit
  • Sistema: Ubuntu 22.04 LTS (aarch64) provisto por JetPack
  • JetPack: 6.0 GA (L4T 36.3)
  • CUDA: 12.2
  • TensorRT: 8.6.2
  • GStreamer: 1.20.x
  • OpenCV para Python: 4.8.x (paquete de Ubuntu o el incluido por JetPack)
  • Python: 3.10
  • Cámara: Arducam USB3.0 Camera (Sony IMX477)

Verifica tu entorno con:

# Verificar L4T/JetPack
cat /etc/nv_tegra_release
# (Opcional) si tienes jetson-stats:
# jetson_release -v

# Kernel y paquetes NVIDIA/TensorRT
uname -a
dpkg -l | grep -E 'nvidia|tensorrt|cuda|cudnn'

# Versiones de Python y librerías clave
python3 -c "import sys; print(sys.version)"
python3 -c "import cv2; print('OpenCV:', cv2.__version__)"
python3 -c "import tensorrt as trt; print('TensorRT:', trt.__version__)"
gst-launch-1.0 --version

Ruta elegida (toolchain de IA): A) TensorRT + ONNX

  • Descargaremos un detector de caras ligero (UltraFace ONNX), construiremos un engine TensorRT con trtexec (FP16) y lo usaremos desde Python para inferencia en tiempo real.
  • Encode H.264 por hardware: nvv4l2h264enc.
  • Servidor RTSP en Python con gst-rtsp-server (GStreamer).

Consejo de potencia y rendimiento en Orin NX (opcional pero recomendado antes de las pruebas):

# Consultar modo de potencia actual
sudo nvpmodel -q
# Ajustar a modo máximo (MAXN)
sudo nvpmodel -m 0
sudo jetson_clocks
# Aviso: monitoriza temperatura con 'tegrastats' para evitar throttling térmico.

Materiales

  • NVIDIA Jetson Orin NX Developer Kit.
  • Cámara Arducam USB3.0 Camera (Sony IMX477), con cable USB 3.0.
  • Fuente de alimentación del kit (suministrada con el Dev Kit).
  • Cable HDMI y monitor (para depuración local, opcional).
  • Red LAN (Ethernet RJ45) o Wi‑Fi configurada en el Jetson.
  • Tarjeta microSD/NVMe con JetPack 6.0 (L4T 36.3) ya instalado.

Nota: Este caso práctico está escrito específicamente para “NVIDIA Jetson Orin NX Developer Kit + Arducam USB3.0 Camera (Sony IMX477)”. No uses CSI ni otros modelos de cámara aquí.

Preparación y conexión

1) Conexión física:
– Conecta la Arducam USB3.0 (IMX477) a un puerto USB3 tipo A del Jetson Orin NX Dev Kit.
– Conecta el Jetson a la red (Ethernet recomendado para menor latencia).
– Alimenta el Jetson con su fuente oficial.

2) Verificación de cámara y formatos:
– Comprueba que el sistema ve la cámara en /dev/videoX (normalmente /dev/video0) y formatos disponibles.

lsusb | grep -i arducam
v4l2-ctl --list-devices
v4l2-ctl -d /dev/video0 --list-formats-ext

3) Tabla de puertos y elementos relevantes:

Elemento Puerto/Interfaz Acción
Arducam USB3.0 (IMX477) USB 3.0 Type‑A (azul) Conectar cámara
Red RJ45 (Ethernet) Conectar LAN (latencia baja para RTSP)
Vídeo local (opcional) HDMI Conectar monitor
Alimentación DC barrel Encender Jetson Orin NX

4) Instalación de dependencias (GStreamer, bindings, V4L, PyCUDA):

sudo apt update
sudo apt install -y \
  python3-opencv python3-gi python3-gst-1.0 \
  gir1.2-gst-rtsp-server-1.0 \
  gstreamer1.0-tools gstreamer1.0-plugins-{good,bad,ugly} \
  v4l-utils python3-pycuda

# Comprobar RTSP server bindings
python3 -c "from gi.repository import Gst, GstRtspServer, GObject; print('GStreamer GI OK')"

Código completo (Python + TensorRT + GStreamer RTSP)

El siguiente script:
– Captura frames de la Arducam (v4l2src) a 1280×720@30.
– Detecta caras con TensorRT (engine de UltraFace FP16).
– Aplica blur en cada ROI de rostro.
– Publica un stream RTSP h264 en rtsp://0.0.0.0:8554/faceblur usando nvv4l2h264enc.

Antes de ejecutarlo, en la sección “Compilación/ejecución” construimos el engine TensorRT ultraface_fp16.plan.

Guarda como rtsp_face_privacy_blur.py:

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

import os
import sys
import time
import threading
import numpy as np
import cv2
import ctypes

import tensorrt as trt
import pycuda.driver as cuda
import pycuda.autoinit  # noqa: F401

from gi.repository import GObject, Gst, GstRtspServer, GLib

# --------- Parámetros globales ----------
VIDEO_W, VIDEO_H, FPS = 1280, 720, 30
RTSP_PORT = 8554
RTSP_MOUNT = "/faceblur"
ENGINE_PATH = "ultraface_fp16.plan"  # generado con trtexec
CONF_THRESH = 0.6
NMS_THRESH = 0.35

# Tamaño de entrada de UltraFace RFB-320
UF_IN_W, UF_IN_H = 320, 240
VARIANCES = [0.1, 0.2]

# Prepara GStreamer
Gst.init(None)

# --------- Utilidades TensorRT ----------
class TRTInfer:
    def __init__(self, engine_path):
        logger = trt.Logger(trt.Logger.WARNING)
        trt.init_libnvinfer_plugins(logger, "")
        with open(engine_path, "rb") as f, trt.Runtime(logger) as runtime:
            self.engine = runtime.deserialize_cuda_engine(f.read())
        assert self.engine is not None, "No se pudo cargar el engine TRT"
        self.context = self.engine.create_execution_context()
        self.stream = cuda.Stream()

        # Bindings
        self.bindings = [None] * self.engine.num_bindings
        self.host_mem = []
        self.device_mem = []
        for i in range(self.engine.num_bindings):
            binding_shape = self.context.get_binding_shape(i)
            if -1 in binding_shape:
                # Fijar shapes dinámicas si procede
                # UltraFace estándar: [1,3,240,320] para input
                if self.engine.binding_is_input(i):
                    self.context.set_binding_shape(i, (1, 3, UF_IN_H, UF_IN_W))
                    binding_shape = self.context.get_binding_shape(i)
            size = trt.volume(binding_shape)
            dtype = trt.nptype(self.engine.get_binding_dtype(i))
            host = cuda.pagelocked_empty(size, dtype)
            device = cuda.mem_alloc(host.nbytes)
            self.host_mem.append(host)
            self.device_mem.append(device)
            self.bindings[i] = int(device)

        # Índices de bindings: suponemos 1 entrada, 2 salidas (loc y conf)
        self.input_idx = 0 if self.engine.binding_is_input(0) else 1
        self.output_idx = [i for i in range(self.engine.num_bindings) if not self.engine.binding_is_input(i)]

    def infer(self, input_chw):
        # Copiar a host buffer
        np.copyto(self.host_mem[self.input_idx], input_chw.ravel())
        # Host->Device
        cuda.memcpy_htod_async(self.device_mem[self.input_idx], self.host_mem[self.input_idx], self.stream)
        # Ejecutar
        self.context.execute_async_v2(self.bindings, self.stream.handle, None)
        # Device->Host para salidas
        outputs = []
        for oi in self.output_idx:
            cuda.memcpy_dtoh_async(self.host_mem[oi], self.device_mem[oi], self.stream)
            outputs.append(np.array(self.host_mem[oi]))
        self.stream.synchronize()
        return outputs

# --------- UltraFace (priors + decode + NMS) ----------
def generate_priors(image_w, image_h):
    # UltraFace RFB-320: strides y min_boxes
    strides = [8, 16, 32, 64]
    min_boxes = [[10, 16, 24],
                 [32, 48],
                 [64, 96],
                 [128, 192, 256]]
    priors = []
    for stride, mbox in zip(strides, min_boxes):
        f_w = int(np.ceil(image_w / stride))
        f_h = int(np.ceil(image_h / stride))
        for i in range(f_h):
            for j in range(f_w):
                cx = (j + 0.5) / f_w
                cy = (i + 0.5) / f_h
                for mb in mbox:
                    w = mb / image_w
                    h = mb / image_h
                    priors.append([cx, cy, w, h])
    priors = np.array(priors, dtype=np.float32)
    return priors

PRIORS = generate_priors(UF_IN_W, UF_IN_H)

def softmax(x, axis=-1):
    x = x - np.max(x, axis=axis, keepdims=True)
    e = np.exp(x)
    return e / np.sum(e, axis=axis, keepdims=True)

def decode_boxes(loc, priors, variances=[0.1, 0.2]):
    # loc: [num_priors,4], priors: [num_priors,4] in [cx,cy,w,h] normalized
    boxes = np.concatenate([
        priors[:, :2] + loc[:, :2] * variances[0] * priors[:, 2:],
        priors[:, 2:] * np.exp(loc[:, 2:] * variances[1])
    ], axis=1)  # [cx,cy,w,h]
    # Convertir a [xmin,ymin,xmax,ymax]
    boxes_xymin = boxes[:, :2] - boxes[:, 2:] / 2
    boxes_xymax = boxes[:, :2] + boxes[:, 2:] / 2
    boxes = np.concatenate([boxes_xymin, boxes_xymax], axis=1)
    return boxes

def nms(boxes, scores, iou_threshold=0.35, top_k=200):
    # boxes: [N,4], scores: [N]
    idxs = scores.argsort()[::-1]
    keep = []
    while idxs.size > 0 and len(keep) < top_k:
        i = idxs[0]
        keep.append(i)
        if idxs.size == 1:
            break
        iou = compute_iou(boxes[i], boxes[idxs[1:]])
        idxs = idxs[1:][iou < iou_threshold]
    return keep

def compute_iou(box, boxes):
    # box: [xmin,ymin,xmax,ymax], boxes: [M,4]
    xx1 = np.maximum(box[0], boxes[:, 0])
    yy1 = np.maximum(box[1], boxes[:, 1])
    xx2 = np.minimum(box[2], boxes[:, 2])
    yy2 = np.minimum(box[3], boxes[:, 3])
    w = np.maximum(0.0, xx2 - xx1)
    h = np.maximum(0.0, yy2 - yy1)
    inter = w * h
    area1 = (box[2] - box[0]) * (box[3] - box[1])
    area2 = (boxes[:, 2] - boxes[:, 0]) * (boxes[:, 3] - boxes[:, 1])
    union = area1 + area2 - inter
    iou = inter / (union + 1e-6)
    return iou

def preprocess_bgr(frame_bgr):
    # Resize manteniendo proporción por padding (letterbox) o simple resize.
    # Para simplicidad, hacemos resize directo a 320x240:
    resized = cv2.resize(frame_bgr, (UF_IN_W, UF_IN_H))
    img = resized.astype(np.float32)
    img = (img - 127.0) / 128.0
    img = img[:, :, ::-1]  # BGR->RGB si el modelo lo requiere; UltraFace usa BGR en muchas impl., pero mantenemos RGB por robustez
    img = np.transpose(img, (2, 0, 1))  # CHW
    img = np.expand_dims(img, axis=0).copy()
    return img, resized

def postprocess(outputs, frame_w, frame_h, conf_thresh=0.6, nms_thresh=0.35):
    # outputs: [loc, conf] sin ordenar; identificamos por tamaño
    # Para UltraFace RFB-320 ONNX típico: conf shape [1,N,2], loc shape [1,N,4]
    loc, conf = None, None
    out0, out1 = outputs
    # Detectar cuál es cuál por su última dimensión
    if out0.size % 2 == 0 and out1.size % 4 == 0:
        conf_raw = out0
        loc_raw = out1
    else:
        conf_raw = out1
        loc_raw = out0
    num_priors = PRIORS.shape[0]
    conf = conf_raw.reshape(1, num_priors, 2)
    loc = loc_raw.reshape(1, num_priors, 4)

    scores = softmax(conf, axis=2)[0, :, 1]
    mask = scores > conf_thresh
    if not np.any(mask):
        return []

    loc = loc[0, mask, :]
    scores = scores[mask]
    priors_sel = PRIORS[mask, :]

    boxes = decode_boxes(loc, priors_sel, variances=VARIANCES)
    # Convertir a pixeles de la imagen original
    boxes[:, [0, 2]] *= frame_w
    boxes[:, [1, 3]] *= frame_h

    # NMS
    keep = nms(boxes, scores, iou_threshold=nms_thresh)
    boxes = boxes[keep]
    scores = scores[keep]

    # Asegurar límites
    boxes[:, 0::2] = np.clip(boxes[:, 0::2], 0, frame_w - 1)
    boxes[:, 1::2] = np.clip(boxes[:, 1::2], 0, frame_h - 1)
    return [(int(xmin), int(ymin), int(xmax), int(ymax), float(sc)) for (xmin, ymin, xmax, ymax), sc in zip(boxes, scores)]

# --------- Captura + inferencia + blur ----------
class FrameProcessor(threading.Thread):
    def __init__(self, device="/dev/video0"):
        super().__init__(daemon=True)
        self.device = device
        self.trt = TRTInfer(ENGINE_PATH)
        self.stop_flag = threading.Event()
        self.lock = threading.Lock()
        self.latest_frame = None
        self.fps = 0.0

        # Construir pipeline GStreamer para la Arducam USB (MJPEG o YUYV)
        # Intentamos MJPEG@720p30 y hacemos decode en CPU -> BGR appsink
        gst_pipeline = (
            f"v4l2src device={self.device} ! "
            f"image/jpeg,framerate={FPS}/1,width={VIDEO_W},height={VIDEO_H} ! "
            "jpegdec ! videoconvert ! video/x-raw,format=BGR ! "
            f"appsink drop=true max-buffers=1 sync=false"
        )
        self.cap = cv2.VideoCapture(gst_pipeline, cv2.CAP_GSTREAMER)
        if not self.cap.isOpened():
            # Fallback a YUYV si MJPEG no está
            gst_pipeline = (
                f"v4l2src device={self.device} ! "
                f"video/x-raw,format=YUY2,framerate={FPS}/1,width={VIDEO_W},height={VIDEO_H} ! "
                "videoconvert ! video/x-raw,format=BGR ! "
                f"appsink drop=true max-buffers=1 sync=false"
            )
            self.cap = cv2.VideoCapture(gst_pipeline, cv2.CAP_GSTREAMER)
        if not self.cap.isOpened():
            raise RuntimeError("No se pudo abrir la cámara Arducam con GStreamer")

    def run(self):
        t0 = time.time()
        frames = 0
        while not self.stop_flag.is_set():
            ret, frame = self.cap.read()
            if not ret:
                time.sleep(0.01)
                continue
            h, w = frame.shape[:2]
            inp, resized = preprocess_bgr(frame)
            outputs = self.trt.infer(inp)
            detections = postprocess(outputs, w, h, CONF_THRESH, NMS_THRESH)

            # Aplicar blur a cada rostro detectado
            for (x1, y1, x2, y2, sc) in detections:
                x1b, y1b = max(0, x1), max(0, y1)
                x2b, y2b = min(w - 1, x2), min(h - 1, y2)
                roi = frame[y1b:y2b, x1b:x2b]
                if roi.size > 0:
                    # Blur gaussiano; kernel proporcional al tamaño del ROI
                    k = max(15, int(min((x2b - x1b), (y2b - y1b)) / 6) | 1)
                    roi_blur = cv2.GaussianBlur(roi, (k, k), 0)
                    frame[y1b:y2b, x1b:x2b] = roi_blur

            with self.lock:
                self.latest_frame = frame.copy()

            frames += 1
            if frames % 30 == 0:
                t1 = time.time()
                self.fps = 30.0 / (t1 - t0)
                t0 = t1

    def read(self):
        with self.lock:
            if self.latest_frame is None:
                # Si aún no hay frame, devuelve un frame negro
                return np.zeros((VIDEO_H, VIDEO_W, 3), dtype=np.uint8)
            return self.latest_frame

    def stop(self):
        self.stop_flag.set()
        self.cap.release()

# --------- RTSP Server con appsrc ----------
class SensorFactory(GstRtspServer.RTSPMediaFactory):
    def __init__(self, frame_provider):
        super(SensorFactory, self).__init__()
        self.frame_provider = frame_provider
        self.number_frames = 0
        self.duration = int(1 / FPS * 1e9)  # nanosegundos por frame
        self.launch_string = (
            "appsrc name=source is-live=true block=true format=time "
            f"caps=video/x-raw,format=BGR,width={VIDEO_W},height={VIDEO_H},framerate={FPS}/1 "
            "! videoconvert ! nvvidconv ! video/x-raw(memory:NVMM),format=NV12 "
            "! nvv4l2h264enc preset-level=1 bitrate=4000000 iframeinterval=30 insert-sps-pps=true "
            "! h264parse config-interval=-1 ! rtph264pay name=pay0 pt=96 config-interval=1"
        )

    def on_need_data(self, src, length):
        frame = self.frame_provider.read()
        data = frame.tobytes()
        buf = Gst.Buffer.new_allocate(None, len(data), None)
        buf.fill(0, data)
        ts = self.number_frames * self.duration
        buf.pts = buf.dts = ts
        buf.duration = self.duration
        self.number_frames += 1
        src.emit("push-buffer", buf)

    def do_create_element(self, url):
        return Gst.parse_launch(self.launch_string)

    def do_configure(self, rtsp_media):
        self.number_frames = 0
        appsrc = rtsp_media.get_element().get_child_by_name("source")
        appsrc.connect("need-data", self.on_need_data)

def main():
    # Procesador de frames con detección y blur
    processor = FrameProcessor("/dev/video0")
    processor.start()

    # Servidor RTSP
    server = GstRtspServer.RTSPServer()
    server.props.service = str(RTSP_PORT)
    factory = SensorFactory(processor)
    factory.set_shared(True)
    m = server.get_mount_points()
    m.add_factory(RTSP_MOUNT, factory)
    server.attach(None)

    print(f"RTSP listo en rtsp://0.0.0.0:{RTSP_PORT}{RTSP_MOUNT}")
    print("Abre con VLC: Media > Open Network Stream > URL anterior")
    print("Presiona Ctrl+C para salir.")

    try:
        loop = GLib.MainLoop()
        loop.run()
    except KeyboardInterrupt:
        pass
    finally:
        processor.stop()

if __name__ == "__main__":
    main()

Explicación breve de partes clave:
– TRTInfer: carga el engine TensorRT y gestiona memoria en GPU (pycuda) para inferencia asíncrona.
– UltraFace utils: generación de priors, decodificación de cajas y NMS sobre la salida del modelo.
– FrameProcessor: hilo que captura frames de la Arducam vía GStreamer, ejecuta inferencia y aplica blur de forma local (OpenCV).
– SensorFactory (RTSP): expone un pipeline RTSP con appsrc → NVENC (nvv4l2h264enc) → RTP/RTSP, empujando los frames difuminados.

Compilación/flash/ejecución

1) Descargar el modelo ONNX (UltraFace RFB-320) y construir el engine TensorRT (FP16):

mkdir -p ~/rtsp-face-privacy-blur && cd ~/rtsp-face-privacy-blur

# Descargar ONNX de UltraFace (RFB-320, entrada 320x240)
wget -O ultraface-rfb-320.onnx \
  "https://github.com/Linzaer/Ultra-Light-Fast-Generic-Face-Detector-1MB/raw/master/models/onnx/version-RFB-320.onnx"

# Construir engine TensorRT FP16 (usa GPU del Orin NX)
# Notas:
#  - --fp16 habilita precisión FP16 (rápida y precisa en Orin NX).
#  - Ajusta --workspace según memoria disponible (MB).
/usr/src/tensorrt/bin/trtexec \
  --onnx=ultraface-rfb-320.onnx \
  --saveEngine=ultraface_fp16.plan \
  --fp16 \
  --workspace=2048 \
  --buildOnly

# (Opcional) Verificar el engine con benchmark sintético
/usr/src/tensorrt/bin/trtexec \
  --loadEngine=ultraface_fp16.plan \
  --shapes=input:1x3x240x320 \
  --fp16 \
  --separateProfileRun \
  --avgRuns=100

2) Colocar el script y ejecutar:

# Copiar el script
nano rtsp_face_privacy_blur.py
# (pega aquí el contenido del script y guarda)

# Ejecutar
python3 rtsp_face_privacy_blur.py

3) Abrir el stream en tu PC:
– Descubre la IP del Jetson:
– ip -o -4 addr show dev eth0 | awk ‘{print $4}’ (o usa ifconfig/ip).
– En VLC (PC): Media → Open Network Stream → rtsp://IP_DEL_JETSON:8554/faceblur
– Con ffplay:

ffplay -rtsp_transport tcp rtsp://IP_DEL_JETSON:8554/faceblur

4) Medición de rendimiento:

# En otra consola del Jetson:
tegrastats

# Modo potencia (si no lo hiciste antes):
sudo nvpmodel -m 0
sudo jetson_clocks

Para detener y volver a modo por defecto:

sudo jetson_clocks --restore
# (elige el modo que prefieras, p. ej., -m 2 según disponibilidad)

Validación paso a paso

1) Cámara detectada:
– v4l2-ctl -d /dev/video0 –all debe mostrar resolución soportada (e.g., 1920×1080 MJPEG/YUYV).
– Si usas MJPEG, verás image/jpeg en los formatos.

2) Engine TensorRT disponible:
– ls -lh ultraface_fp16.plan debe existir (~5–15 MB).
– trtexec –loadEngine=… no debe reportar errores.

3) Servidor RTSP en marcha:
– Al ejecutar python3 rtsp_face_privacy_blur.py, el terminal mostrará:
– “RTSP listo en rtsp://0.0.0.0:8554/faceblur”
– Si añadiste prints de FPS, verás “FPS ~ XX” cada pocos segundos (puedes ampliarlo en el hilo).

4) Stream reproducible:
– VLC reproduce el stream con latencia baja; verifica que los rostros se ven difuminados.
– Si te acercas a la cámara o miras de frente, la cara debe detectarse y aplicar blur dentro de 1–2 frames.

5) Métricas cuantitativas esperadas:
– tegrastats: GPU 10–35% (según escena), EMC razonable, RAM estable.
– FPS en 1280×720: 22–30 FPS típicamente con UltraFace FP16 en Orin NX (iluminación normal).
– Latencia (LAN cableada): ~120–220 ms (depende de encoder/decodificador y reproductor).
– Porcentaje de frames con al menos una cara correctamente difuminada cuando hay rostro visible a cámara: > 90% en interiores.

6) Calidad del blur:
– El ROI debe cubrir rasgos clave; ajusta CONF_THRESH (0.6 por defecto) si detecta de más/de menos.
– Kernel del blur se adapta al tamaño del ROI; verifica que oculta rasgos.

Troubleshooting (5–8 problemas típicos y soluciones)

1) No se abre la cámara (No se pudo abrir la cámara Arducam con GStreamer):
– Comprueba /dev/video0 existe y permisos: ls -l /dev/video0
– Lista formatos: v4l2-ctl -d /dev/video0 –list-formats-ext
– Cambia la rama del pipeline a YUYV si MJPEG no está disponible (el script ya hace fallback).
– Prueba una resolución soportada menor: 1280×720 → 640×480.
– Verifica que el cable y el puerto USB3 funcionan (prueba en otro puerto azul).

2) VLC no reproduce el RTSP:
– Asegura que el puerto 8554 no está bloqueado por firewall (ufw, router).
– Prueba con ffplay -rtsp_transport tcp rtsp://IP:8554/faceblur
– Comprueba que el script corre y no hay errores en consola.
– Cambia a red Ethernet si estás en Wi‑Fi con alta latencia.

3) Engine TensorRT incompatible (error al deserializar):
– Asegúrate de construir el engine en la misma plataforma/versión de TensorRT donde lo ejecutas (Jetson Orin NX con TRT 8.6.x).
– Reconstruye el engine con el trtexec del Jetson actual (no copies engines de otras máquinas).
– Verifica ruta de entrada del engine y shapes.

4) Bajo rendimiento/FPS inestable:
– Activa MAXN y jetson_clocks.
– Baja la resolución de salida a 960×540 o 640×480.
– Reduce CONF_THRESH si hay demasiados candidatos falsos; eleva NMS_THRESH si hay solapamiento excesivo.
– Asegura que appsink drop=true max-buffers=1 esté activo para no acumular latencia.

5) Difuminado fuera de lugar o cajas incorrectas:
– El modelo UltraFace espera entrada 320×240; mantén estos valores en preprocess.
– Comprueba que la conversión BGR/RGB sea acorde; si notas desplazamientos, cambia la línea img = img[:, :, ::-1] (prueba BGR puro).
– Verifica escalado de boxes: multiplicamos [x, y] por [w, h] de frame original.

6) Artefactos de color o errores del encoder:
– Asegura que nvvidconv produce NV12 en memoria NVMM antes de nvv4l2h264enc.
– Si tu versión de GStreamer difiere, prueba preset-level=1 → preset-level=2 o añade control-rate=1 (CBR).
– En redes saturadas, incrementa bitrate a 6–8 Mbps.

7) “Could not load GStreamer RTSP server bindings”:
– Reinstala paquetes GI: sudo apt install -y python3-gi gir1.2-gst-rtsp-server-1.0
– Comprueba que Gst.init(None) no devuelve error.

8) Calentamiento/térmica:
– Monitoriza con tegrastats; si la temperatura sube demasiado, retira jetson_clocks o usa un perfil nvpmodel más conservador (p. ej., -m 2).
– Mejora ventilación del Dev Kit.

Mejoras/variantes

  • Autenticación RTSP: añade digest/basic auth con GstRtspServer (crear RTSPAuth y asignar credenciales).
  • Multiples calidades: crea dos factories (720p y 480p) en rtsp://…/faceblur y /faceblur_low.
  • INT8 para mayor rendimiento: calibrar el modelo UltraFace y construir engine INT8 con trtexec –int8 –calib. Requiere dataset de calibración.
  • Cambiar difuminado por pixelado (mosaico) o por cubrimiento sólido (“redaction boxes”).
  • Seguimiento de caras con centroid tracker para suavizar cajas en secuencia y reducir parpadeos (flicker).
  • Registro de métricas Prometheus: FPS, latencia de inferencia, número de rostros detectados, etc.
  • Servicio systemd: crear unidad para arrancar el RTSP on boot y reinicio automático en fallo.
  • Ajuste a 1080p: Si la Arducam entrega 1920×1080@30 y el Orin NX lo soporta con suficiente FPS, sube VIDEO_W/H y el bitrate del encoder.

Checklist de verificación

  • [ ] JetPack 6.0 (L4T 36.3) y TensorRT 8.6.x verificados en el Jetson Orin NX.
  • [ ] Arducam USB3.0 (IMX477) detectada en /dev/video0 con v4l2-ctl.
  • [ ] Modelo ONNX UltraFace descargado y engine FP16 generado con trtexec.
  • [ ] Dependencias instaladas: python3-opencv, python3-gi, gstreamer-plugins, python3-pycuda.
  • [ ] Script rtsp_face_privacy_blur.py ejecutándose sin errores.
  • [ ] Reproducción RTSP en VLC/ffplay con blur aplicado a cada rostro.
  • [ ] FPS, GPU y CPU dentro de rangos esperados; sin sobrecalentamiento.
  • [ ] nvpmodel/jetson_clocks revertidos si es necesario tras pruebas.

Apéndice: Comandos de rendimiento y limpieza

  • Seguimiento de recursos durante la ejecución:
tegrastats --interval 1000
  • Prueba de red y latencia:
ping -c 5 IP_DEL_CLIENTE
  • Revertir ajustes de potencia al finalizar:
sudo jetson_clocks --restore
# Selecciona modo adecuado si cambiaste nvpmodel
sudo nvpmodel -q

Notas finales

  • Este caso práctico mantiene coherencia estricta con el hardware: “NVIDIA Jetson Orin NX Developer Kit + Arducam USB3.0 Camera (Sony IMX477)”.
  • El camino elegido es A) TensorRT + ONNX con engine FP16 y servidor RTSP con GStreamer. No se requieren herramientas externas ni GUI.
  • El pipeline se basa en:
  • Captura V4L2 (vía GStreamer) desde la Arducam USB3.0.
  • Preprocesado y detección de caras con UltraFace (TensorRT FP16).
  • Difuminado con OpenCV.
  • Codificación H.264 con nvv4l2h264enc y publicación RTSP con gst-rtsp-server.

Con este flujo tendrás un RTSP “privacy by design” para caras, apto para escenarios reales de laboratorio y prototipado, y con aceleración GPU en el NVIDIA Jetson Orin NX.

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 servidor RTSP en NVIDIA Jetson Orin NX?




Pregunta 2: ¿Qué tipo de cámara se utiliza en este proyecto?




Pregunta 3: ¿Cuál es la latencia máxima permitida en el streaming LAN?




Pregunta 4: ¿Qué porcentaje de detección de caras se espera en interiores bien iluminados?




Pregunta 5: ¿Qué tecnología se utiliza para aplicar el difuminado en las caras detectadas?




Pregunta 6: ¿Cuál es el formato de codificación utilizado para el vídeo?




Pregunta 7: ¿Cuál es el público objetivo de este proyecto?




Pregunta 8: ¿Qué se espera en términos de FPS sostenido?




Pregunta 9: ¿Qué sistema operativo es necesario para la reproducibilidad del caso práctico?




Pregunta 10: ¿Qué se busca lograr al generar datasets anónimos de vídeo?




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

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

Sígueme:
Scroll al inicio