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




