Objetivo y caso de uso
Qué construirás: Un nodo perimetral en Jetson Orin Nano 8GB que fusiona visión RGB (YOLOv5s TensorRT) con térmica y BME680 para detectar intrusiones y emitir alertas con evidencia en tiempo real. Acelerado por GPU (FP16) y optimizado para baja latencia.
Para qué sirve
- Vigilancia de perímetros en instalaciones industriales: detectar una persona cruzando una línea de seguridad en exteriores, reduciendo falsos positivos por vegetación o sombras.
- Protección de obra civil nocturna: activar una alerta solo cuando la detección de “persona” se acompaña de una firma térmica superior al ambiente.
- Monitoreo de vallados en granjas o subestaciones: disparar eventos con evidencia (frames anotados y métricas) y registrar IAQ/humedad para correlacionar con falsas alarmas por niebla o polvo.
- Control de accesos en almacenes: supervisar zonas de carga con barreras virtuales en ROI definidos y validación de temperatura para evitar alarmas por animales pequeños o reflejos.
Resultado esperado
- FPS sostenido de la inferencia RGB ≥ 25 FPS con YOLOv5s FP.
- Latencia de detección de intrusiones.
- Precisión de detección superior al 90% en condiciones de luz variable.
- Registro de datos de temperatura y humedad cada 5 segundos para análisis de IAQ.
Público objetivo: Desarrolladores y profesionales de seguridad; Nivel: Avanzado
Arquitectura/flujo: Integración de sensores RGB y térmicos con procesamiento en Jetson Orin Nano mediante pipelines
Nivel: Avanzado
Prerrequisitos (SO, toolchain y versiones exactas)
Se asume NVIDIA Jetson Orin Nano 8GB con JetPack (L4T) en Ubuntu:
- Sistema base y SDK:
- Ubuntu 20.04.6 LTS (arm64)
- JetPack 5.1.2 (L4T 35.4.1)
- CUDA 11.4
- cuDNN 8.6.0
- TensorRT 8.5.2.2
- GStreamer 1.16.x
- Python 3.8.10
- OpenCV vía apt (python3-opencv 4.2.x en Ubuntu 20.04)
- Cámara CSI gestionada por nvargus (IMX477 soportado por libargus con drivers de Arducam/IMX477).
- Herramientas de rendimiento:
- nvpmodel, jetson_clocks
- tegrastats
Verifica tus versiones con:
cat /etc/nv_tegra_release
uname -a
dpkg -l | grep -E 'nvidia|tensorrt'
python3 -c "import cv2, sys; print('OpenCV', cv2.__version__); import tensorrt as trt; print('TensorRT', trt.__version__); import torch, pkgutil; print('torch?', 'torch' in [m.name for m in pkgutil.iter_modules()])"
Ejemplo esperado (aproximado):
– L4T R35.4.1, Jetson-5.1.2
– TensorRT 8.5.2
– OpenCV 4.2.x
Elegimos ruta A) TensorRT + ONNX (una sola ruta consistente), con motor FP16.
Materiales
- NVIDIA Jetson Orin Nano 8GB (módulo + carrier con conector CSI y cabecera de 40 pines).
- Arducam IMX477 (Raspberry Pi HQ Camera compatible con Jetson, conexión CSI).
- MLX90640 (matriz térmica 32×24, interfaz I2C, dirección típica 0x33).
- BME680 (sensor ambiental: temperatura, humedad, presión, gas/IAQ, I2C, dirección 0x76 u 0x77).
- Cables:
- Cable CSI para la IMX477.
- Cables Dupont para I2C (SDA/SCL + 3V3 + GND).
- Alimentación adecuada para Orin Nano (≥ 5 A recomendados).
- Tarjeta microSD/SSD configurada con Ubuntu 20.04 + JetPack 5.1.2.
Modelo exacto a usar en todo el caso: NVIDIA Jetson Orin Nano 8GB + Arducam IMX477 + MLX90640 + BME680.
Preparación y conexión
Conexión física
- Conecta la Arducam IMX477 al puerto CSI de la carrier board (asegúrate de la orientación del cable flex).
- Conecta MLX90640 y BME680 al bus I2C de la cabecera de 40 pines del Jetson Orin Nano (bus 1: /dev/i2c-1):
Tabla de pines (cabecera de 40 pines, vista estándar del Jetson; revisa el pinout de tu carrier):
| Señal | Pin Jetson | Función | MLX90640 | BME680 | Notas |
|---|---|---|---|---|---|
| 3V3 | 1 | Alimentación 3.3 V | VCC | VCC | MLX90640 y BME680 funcionan a 3V3 |
| 5V | 2 | Alimentación 5 V | — | — | No conectar a sensores I2C 3V3 |
| SDA | 3 | I2C-1 SDA (/dev/i2c-1) | SDA | SDA | Pull-ups en placa; típicamente no necesarios externos |
| SCL | 5 | I2C-1 SCL (/dev/i2c-1) | SCL | SCL | Frecuencia 100–400 kHz |
| GND | 6 | Tierra | GND | GND | Común |
| — | — | — | — | — | — |
Direcciones I2C típicas:
– MLX90640: 0x33
– BME680: 0x76 (algunas placas: 0x77, configurable con pad/jumper)
Comprobaciones iniciales
1) Cámara CSI:
– Reinicia el demonio Argus y lista el dispositivo.
sudo systemctl restart nvargus-daemon
v4l2-ctl --list-devices
- Prueba rápida con GStreamer (ventana X opcional):
gst-launch-1.0 nvarguscamerasrc sensor-id=0 ! \
"video/x-raw(memory:NVMM),width=1280,height=720,framerate=30/1,format=NV12" ! \
nvvidconv ! "video/x-raw,format=I420" ! fakesink
Si no hay errores, el sensor está visible por nvargus.
2) I2C:
sudo apt-get update
sudo apt-get install -y i2c-tools
i2cdetect -y -r 1
Debes ver 0x33 (MLX90640) y 0x76 (BME680). Si ves 0x77 para BME680, ajusta la dirección en el código.
Configuración software y librerías
- Paquetes base:
sudo apt-get install -y python3-pip python3-dev build-essential \
python3-opencv gstreamer1.0-tools gstreamer1.0-plugins-good \
gstreamer1.0-plugins-bad gstreamer1.0-libav v4l-utils
- Librerías Python para sensores:
pip3 install --upgrade pip
pip3 install adafruit-blinka adafruit-circuitpython-mlx90640 adafruit-circuitpython-bme680 numpy pycuda
Añade tu usuario al grupo i2c (cierra sesión y vuelve a entrar):
sudo usermod -aG i2c $USER
- Verifica TensorRT en Python:
python3 -c "import tensorrt as trt; print(trt.__version__)"
- Prepara modo de potencia y clocks para rendimiento:
sudo nvpmodel -q
sudo nvpmodel -m 0
sudo jetson_clocks
Advertencia: supervisa térmicas/ventilación.
Código completo
A continuación, un script Python “rgb_thermal_perimeter.py” que:
– Captura video de la IMX477 con GStreamer en 1280×720.
– Ejecuta YOLOv5s (TensorRT FP16) sobre frames (entrada 640×640).
– Lee térmica MLX90640 y ambiental BME680.
– Fusiona: dispara “intrusión” si hay detección de “person” en ROI perimetral y ΔT (ROI vs ambiente) supera umbral adaptado por humedad.
– Registra FPS, GPU load (opcional por tegrastats) y eventos.
Requisitos previos: construir motor TensorRT FP16 (ver sección de compilación).
#!/usr/bin/env python3
# rgb_thermal_perimeter.py
import os
import sys
import time
import math
import ctypes
import threading
import numpy as np
import cv2
# TensorRT + PyCUDA
import tensorrt as trt
import pycuda.driver as cuda
import pycuda.autoinit # noqa: F401
# Sensores I2C
import board
import busio
import adafruit_mlx90640
import adafruit_bme680
# ----------------------------
# Configuración general
# ----------------------------
ENGINE_PATH = "./yolov5s_fp16.engine" # construido con trtexec
CONF_THRESH = 0.40
IOU_THRESH = 0.45
INPUT_W = 640
INPUT_H = 640
# ROI perimetral: banda inferior del frame (en coords de frame 1280x720)
# Se puede ajustar dinamicamente.
ROI_Y_RATIO = 0.75 # 75% alto hacia abajo
ROI_HEIGHT_RATIO = 0.25 # banda de 25% inferior
# Umbral térmico base (ΔT) a sumar a la temperatura ambiente
DELTA_T_BASE = 5.0 # °C
DELTA_T_HUMIDITY_BONUS = -2.0 # reduce umbral si humedad alta (>80%)
# Dirección I2C BME680
BME680_ADDR = 0x76 # cambiar a 0x77 si aplica
# ----------------------------
# Utilidades YOLOv5
# ----------------------------
def letterbox(im, new_shape=(640, 640), color=(114, 114, 114), auto=False, scaleFill=False, scaleup=True):
shape = im.shape[:2] # (h,w)
if isinstance(new_shape, int):
new_shape = (new_shape, new_shape)
r = min(new_shape[0] / shape[0], new_shape[1] / shape[1])
if not scaleup:
r = min(r, 1.0)
new_unpad = (int(round(shape[1] * r)), int(round(shape[0] * r)))
dw, dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1]
dw /= 2
dh /= 2
if shape[::-1] != new_unpad:
im = cv2.resize(im, new_unpad, interpolation=cv2.INTER_LINEAR)
top, bottom = int(round(dh-0.1)), int(round(dh+0.1))
left, right = int(round(dw-0.1)), int(round(dw+0.1))
im = cv2.copyMakeBorder(im, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color)
return im, r, (dw, dh)
def non_max_suppression(prediction, conf_thres=0.25, iou_thres=0.45):
# prediction: [N, 85] (x,y,w,h,conf,cls...)
x = prediction
conf = x[:, 4]
x = x[conf > conf_thres]
if x.shape[0] == 0:
return []
conf = x[:, 4:5]
cls_conf = x[:, 5:]
cls_ids = np.argmax(cls_conf, axis=1)
cls_scores = cls_conf[np.arange(cls_conf.shape[0]), cls_ids]
scores = conf.squeeze() * cls_scores
boxes = xywh2xyxy(x[:, 0:4])
# NMS básico
keep = nms_numpy(boxes, scores, iou_thres)
if len(keep) == 0:
return []
out = []
for i in keep:
out.append([boxes[i, 0], boxes[i, 1], boxes[i, 2], boxes[i, 3], scores[i], cls_ids[i]])
return np.array(out)
def xywh2xyxy(x):
# x: [N,4]
y = np.zeros_like(x)
y[:, 0] = x[:, 0] - x[:, 2] / 2 # x1
y[:, 1] = x[:, 1] - x[:, 3] / 2 # y1
y[:, 2] = x[:, 0] + x[:, 2] / 2 # x2
y[:, 3] = x[:, 1] + x[:, 3] / 2 # y2
return y
def nms_numpy(boxes, scores, iou_threshold):
idxs = scores.argsort()[::-1]
selected = []
while len(idxs) > 0:
i = idxs[0]
selected.append(i)
if len(idxs) == 1:
break
ious = bbox_iou(boxes[i], boxes[idxs[1:]])
idxs = idxs[1:][ious < iou_threshold]
return selected
def bbox_iou(box1, boxes2):
# box1: [4], boxes2: [M,4]
x11, y11, x12, y12 = box1
x21, y21, x22, y22 = boxes2.T
xa1 = np.maximum(x11, x21)
ya1 = np.maximum(y11, y21)
xa2 = np.minimum(x12, x22)
ya2 = np.minimum(y12, y22)
inter = np.maximum(0, xa2 - xa1) * np.maximum(0, ya2 - ya1)
area1 = (x12 - x11) * (y12 - y11)
area2 = (x22 - x21) * (y22 - y21)
union = area1 + area2 - inter
return inter / (union + 1e-7)
# ----------------------------
# Motor TensorRT
# ----------------------------
class TRTInfer:
def __init__(self, engine_path):
self.logger = trt.Logger(trt.Logger.WARNING)
trt.init_libnvinfer_plugins(self.logger, "")
with open(engine_path, "rb") as f, trt.Runtime(self.logger) as runtime:
self.engine = runtime.deserialize_cuda_engine(f.read())
self.context = self.engine.create_execution_context()
# Bindings
self.inputs = []
self.outputs = []
self.allocations = []
for i in range(self.engine.num_bindings):
name = self.engine.get_binding_name(i)
dtype = trt.nptype(self.engine.get_binding_dtype(i))
shape = self.context.get_binding_shape(i)
if -1 in shape:
# Define shapes dinámicas
if self.engine.binding_is_input(i):
self.context.set_binding_shape(i, (1, 3, INPUT_H, INPUT_W))
shape = (1, 3, INPUT_H, INPUT_W)
else:
# salida YOLOv5s típica: (1, 25200, 85)
# El engine debería codificar esto; se deja al runtime.
pass
size = trt.volume(shape)
host_mem = cuda.pagelocked_empty(size, dtype)
device_mem = cuda.mem_alloc(host_mem.nbytes)
self.allocations.append(device_mem)
binding = {
"index": i,
"name": name,
"dtype": dtype,
"shape": shape,
"host_mem": host_mem,
"device_mem": device_mem,
"is_input": self.engine.binding_is_input(i)
}
if binding["is_input"]:
self.inputs.append(binding)
else:
self.outputs.append(binding)
self.stream = cuda.Stream()
def infer(self, img):
# img: numpy float32 [1,3,640,640], normalizado [0,1], RGB
np.copyto(self.inputs[0]["host_mem"], img.ravel())
cuda.memcpy_htod_async(self.inputs[0]["device_mem"], self.inputs[0]["host_mem"], self.stream)
bindings = [int(b["device_mem"]) for b in sorted(self.inputs + self.outputs, key=lambda x: x["index"])]
self.context.execute_async_v2(bindings, self.stream.handle)
for out in self.outputs:
cuda.memcpy_dtoh_async(out["host_mem"], out["device_mem"], self.stream)
self.stream.synchronize()
return [out["host_mem"] for out in self.outputs]
# ----------------------------
# Sensores I2C setup
# ----------------------------
def setup_sensors():
i2c = busio.I2C(board.SCL, board.SDA, frequency=400000)
mlx = adafruit_mlx90640.MLX90640(i2c)
mlx.refresh_rate = adafruit_mlx90640.RefreshRate.REFRESH_8_HZ
bme = adafruit_bme680.Adafruit_BME680_I2C(i2c, address=BME680_ADDR)
bme.sea_level_pressure = 1013.25
return mlx, bme
def read_mlx90640(mlx):
frame = [0] * 768
try:
mlx.getFrame(frame)
arr = np.array(frame).reshape((24, 32)) # 24 filas x 32 columnas
return arr
except Exception:
return None
def read_bme680(bme):
try:
return {
"temperature": float(bme.temperature),
"humidity": float(bme.humidity),
"pressure": float(bme.pressure),
"gas": float(bme.gas)
}
except Exception:
return None
# ----------------------------
# GStreamer capture
# ----------------------------
def build_gst_pipeline(width=1280, height=720, fps=30):
return (
f"nvarguscamerasrc sensor-id=0 bufapi-version=true ! "
f"video/x-raw(memory:NVMM), width={width}, height={height}, framerate={fps}/1, format=NV12 ! "
"nvvidconv flip-method=0 ! video/x-raw, format=BGRx ! "
"videoconvert ! video/x-raw, format=BGR ! appsink drop=true max-buffers=1"
)
# ----------------------------
# Fusión perimetral
# ----------------------------
def fuse_intrusion(dets, rgb_shape, thermal, ambient_temp, humidity):
# dets: [N,6] -> x1,y1,x2,y2,score,cls
# ROI perimetral en coords del RGB (1280x720)
H, W = rgb_shape[:2]
y0 = int(H * ROI_Y_RATIO)
y1 = int(min(H, y0 + H * ROI_HEIGHT_RATIO))
roi_rgb = (0, y0, W, y1)
# Umbral térmico adaptado por humedad
delta_t = DELTA_T_BASE + (DELTA_T_HUMIDITY_BONUS if humidity is not None and humidity > 80.0 else 0.0)
# Ajuste grosero de FOV: mapeamos ROI de RGB a térmica en proporción (sin calibración)
# asumimos campos de visión similares y centrados.
# thermal: 24x32
th_h, th_w = thermal.shape if thermal is not None else (24, 32)
def rgb_box_to_thermal_box(box):
x1, y1b, x2, y2b = box
rx1 = int((x1 / W) * th_w)
rx2 = int((x2 / W) * th_w)
ry1 = int((y1b / H) * th_h)
ry2 = int((y2b / H) * th_h)
rx1 = max(0, min(th_w - 1, rx1))
rx2 = max(0, min(th_w, rx2))
ry1 = max(0, min(th_h - 1, ry1))
ry2 = max(0, min(th_h, ry2))
return rx1, ry1, rx2, ry2
events = []
if dets is None or len(dets) == 0 or thermal is None or ambient_temp is None:
return events, roi_rgb, delta_t
for d in dets:
x1, y1b, x2, y2b, score, cls_id = d
# Clase "person" en COCO es 0 en YOLOv5
if int(cls_id) != 0 or score < CONF_THRESH:
continue
# intersección con ROI perimetral
if y2b < roi_rgb[1] or y1b > roi_rgb[3]:
continue # no toca la banda inferior
# Mapea bbox a térmica
tx1, ty1, tx2, ty2 = rgb_box_to_thermal_box((x1, y1b, x2, y2b))
patch = thermal[ty1:ty2, tx1:tx2]
if patch.size == 0:
continue
t_obj = float(np.nanmean(patch))
# Intrusión si t_obj > ambient + delta_t
if t_obj > ambient_temp + delta_t:
events.append({
"bbox": (int(x1), int(y1b), int(x2), int(y2b)),
"score": float(score),
"cls_id": int(cls_id),
"t_obj": t_obj,
"t_ambient": ambient_temp,
"delta_t": delta_t
})
return events, roi_rgb, delta_t
# ----------------------------
# Main loop
# ----------------------------
def main():
if not os.path.exists(ENGINE_PATH):
print(f"ERROR: motor TensorRT no encontrado en {ENGINE_PATH}")
sys.exit(1)
cap = cv2.VideoCapture(build_gst_pipeline(), cv2.CAP_GSTREAMER)
if not cap.isOpened():
print("ERROR: no se puede abrir la cámara IMX477 vía GStreamer/nvargus.")
sys.exit(1)
trt_infer = TRTInfer(ENGINE_PATH)
mlx, bme = setup_sensors()
frame_count = 0
t0 = time.perf_counter()
last_thermal = None
last_env = None
last_thermal_t = 0.0
last_env_t = 0.0
print("Iniciando bucle. 'q' para salir.")
while True:
ok, frame = cap.read()
if not ok:
print("WARN: frame no disponible")
continue
H, W = frame.shape[:2]
# Prepro YOLO
img, r, (dw, dh) = letterbox(frame, (INPUT_H, INPUT_W))
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img = img.astype(np.float32) / 255.0
img = np.transpose(img, (2, 0, 1))
img = np.expand_dims(img, 0).copy()
# Inferencia TRT
t_infer0 = time.perf_counter()
outputs = trt_infer.infer(img)
t_infer1 = time.perf_counter()
# Asumimos único output: [1, N, 85]
out = outputs[0].reshape(1, -1, 85)
preds = out[0]
# Postpro YOLO
dets = non_max_suppression(preds, CONF_THRESH, IOU_THRESH)
# Lecturas sensores con throttling (no más de 8–10 Hz)
now = time.perf_counter()
if now - last_thermal_t > 0.12:
th = read_mlx90640(mlx)
if th is not None:
last_thermal = th
last_thermal_t = now
if now - last_env_t > 0.5:
env = read_bme680(bme)
if env is not None:
last_env = env
last_env_t = now
ambient_temp = last_env["temperature"] if last_env else None
humidity = last_env["humidity"] if last_env else None
events, roi_rgb, delta_t = fuse_intrusion(dets, frame.shape, last_thermal, ambient_temp, humidity)
# Overlay para visualización/validación
# Dibuja ROI perimetral
cv2.rectangle(frame, (roi_rgb[0], roi_rgb[1]), (roi_rgb[2], roi_rgb[3]), (0, 255, 255), 2)
# Dibuja detecciones
if dets is not None and len(dets) > 0:
for d in dets:
x1, y1b, x2, y2b, score, cls_id = d.astype(int)
color = (0, 255, 0) if int(cls_id) == 0 else (255, 0, 0)
cv2.rectangle(frame, (x1, y1b), (x2, y2b), color, 2)
# Dibuja eventos
for ev in events:
x1, y1b, x2, y2b = ev["bbox"]
label = f"INTRUSION person {ev['score']:.2f} dT>{ev['delta_t']:.1f}C Tobj:{ev['t_obj']:.1f}C"
cv2.putText(frame, label, (x1, max(0, y1b-10)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0,0,255), 2)
# HUD con métricas
fps_inst = 1.0 / max(1e-3, (t_infer1 - t_infer0))
txt = f"FPS_infer:{fps_inst:.1f} TempAmb:{ambient_temp:.1f if ambient_temp else float('nan')}C Hum:{humidity:.1f if humidity else float('nan')}%"
cv2.putText(frame, txt, (10, 24), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255,255,255), 2)
# Muestra (si tienes X/GUI), si no, comentar:
# cv2.imshow("rgb-thermal-perimeter-intrusion", frame)
# if cv2.waitKey(1) & 0xFF == ord('q'):
# break
# Log por stdout (modo headless)
if len(events) > 0:
print(f"[EVENT] {time.strftime('%F %T')} intrusions={len(events)} ambient={ambient_temp:.1f if ambient_temp else -1}C hum={humidity:.1f if humidity else -1}%")
frame_count += 1
cap.release()
cv2.destroyAllWindows()
if __name__ == "__main__":
main()
Explicación breve de las partes clave:
– TRTInfer: carga el engine TensorRT FP16 y prepara bindings de entrada/salida; infer ejecuta de modo asincrónico con stream CUDA.
– Pre/postprocesamiento YOLOv5: letterbox, normalización y NMS; salida típica [1,25200,85] con boxes xywh, conf y 80 clases COCO.
– Fusión: se define un ROI perimetral en la banda inferior del frame; se mapea grosso modo a la rejilla térmica 24×32 para estimar temperatura del objeto y compararla con ambiente + ΔT adaptado por humedad.
– Sensores: MLX90640 a ~8 Hz; BME680 a ~2 Hz para no sobrecargar I2C; se guarda la última lectura válida y se publica en overlay/log.
Código auxiliar para comprobar sensores (opcional, útil para diagnóstico):
#!/usr/bin/env python3
# sensor_check.py
import board, busio, time, numpy as np
import adafruit_mlx90640, adafruit_bme680
i2c = busio.I2C(board.SCL, board.SDA, frequency=400000)
mlx = adafruit_mlx90640.MLX90640(i2c)
mlx.refresh_rate = adafruit_mlx90640.RefreshRate.REFRESH_8_HZ
bme = adafruit_bme680.Adafruit_BME680_I2C(i2c, address=0x76)
for i in range(5):
frame = [0]*768
mlx.getFrame(frame)
arr = np.array(frame).reshape((24,32))
print(f"MLX90640: min={np.min(arr):.1f}C max={np.max(arr):.1f}C mean={np.mean(arr):.1f}C")
print(f"BME680: T={bme.temperature:.1f}C H={bme.humidity:.1f}% P={bme.pressure:.1f}hPa Gas={bme.gas:.0f}ohms")
time.sleep(0.5)
Compilación/flash/ejecución
1) Verifica JetPack y modo potencia
cat /etc/nv_tegra_release
sudo nvpmodel -m 0
sudo jetson_clocks
2) Descarga modelo ONNX y construye motor TensorRT FP16
Usaremos YOLOv5s (COCO, 640). Descarga ONNX oficial y crea engine:
mkdir -p ~/models && cd ~/models
wget -O yolov5s.onnx https://github.com/ultralytics/yolov5/releases/download/v6.2/yolov5s.onnx
# Verifica tamaño ~ 14–15 MB
ls -lh yolov5s.onnx
Construye motor FP16 con trtexec (TensorRT 8.5.2 en JetPack 5.1.2):
/usr/src/tensorrt/bin/trtexec \
--onnx=yolov5s.onnx \
--saveEngine=yolov5s_fp16.engine \
--explicitBatch \
--fp16 \
--workspace=2048 \
--shapes=images:1x3x640x640 \
--verbose
Notas:
– El input en muchos exportes se llama “images”; si tu ONNX tiene otro nombre (p.ej. “input”), ajusta –shapes.
– FP16 aprovecha los Tensor Cores del Orin Nano 8GB.
– El engine generado es específico de la versión de TensorRT/L4T.
Copia el engine junto al script:
cp yolov5s_fp16.engine ~/rgb-thermal-perimeter-intrusion/
3) Prepara el proyecto y dependencias Python
mkdir -p ~/rgb-thermal-perimeter-intrusion && cd ~/rgb-thermal-perimeter-intrusion
# Copia los scripts rgb_thermal_perimeter.py y sensor_check.py aquí.
pip3 install --upgrade numpy pycuda adafruit-blinka adafruit-circuitpython-mlx90640 adafruit-circuitpython-bme680
4) Pruebas de cámara y sensores
- Cámara (headless):
gst-launch-1.0 nvarguscamerasrc sensor-id=0 ! \
"video/x-raw(memory:NVMM), width=1280, height=720, framerate=30/1, format=NV12" ! \
nvvidconv ! "video/x-raw,format=I420" ! fakesink
- I2C:
i2cdetect -y -r 1
python3 sensor_check.py
5) Ejecuta la aplicación
En una terminal, monitorea recursos:
sudo tegrastats --interval 1000
En otra, lanza la app:
cd ~/rgb-thermal-perimeter-intrusion
python3 rgb_thermal_perimeter.py
Parámetros (si deseas ajustar ROI/umbrales), edítalos al inicio del script.
Salida esperada en consola (ejemplo):
– Logs de eventos: “[EVENT] 2025-01-01 12:00:00 intrusions=1 ambient=22.4C hum=65.1%”
– FPS de inferencia ~ 25–35 FPS (según escena/resolución/overlays).
– tegrastats mostrando uso de GPU (GR3D), memoria y clocks.
Para detener:
– Ctrl+C en la app y termina tegrastats con Ctrl+C.
– Revertir clocks/power (opcional):
sudo nvpmodel -m 1 # modo más eficiente (según disponibilidad)
Validación paso a paso
Verificación 1: Integridad del entorno
- Comando: cat /etc/nv_tegra_release → Debe mostrar R35.4.1 (JetPack 5.1.2).
- Comando: dpkg -l | grep tensorrt → Debe listar tensorrt 8.5.x.
Criterio: versiones coinciden con las declaradas.
Verificación 2: Cámara IMX477 operativa
- Comando: gst-launch-1.0 nvarguscamerasrc … fakesink → Debe ejecutar sin errores “Argus”.
- Alternativa (con X): usa xvimagesink para visualizar.
Criterio: no hay errores “No cameras available” ni timeouts.
Verificación 3: Sensores I2C visibles
- Comando: i2cdetect -y -r 1 → Deben aparecer 0x33 y 0x76/0x77.
- Comando: python3 sensor_check.py → Muestra min/max/mean de MLX90640 y T/H/P/Gas de BME680 con valores razonables.
Criterio: lecturas no NaN y en rangos plausibles (T 10–40 C, Hum 20–90%, etc.).
Verificación 4: Inferencia TensorRT
- Comando: /usr/src/tensorrt/bin/trtexec –loadEngine=yolov5s_fp16.engine –shapes=images:1x3x640x640 –dumpProfile
- Debe mostrar tiempos por capa y FPS estimado.
- Ejecuta la app y observa “FPS_infer: XX.X” en el overlay/log.
Criterio: FPS ≥ 25 en modo MAXN y resolución 1280×720 (entrada 640×640 para modelo).
Verificación 5: Fusión y disparo de intrusión
- Sitúa una persona cruzando la banda inferior del frame (ROI).
- Con ambiente estable, verifica:
- El bounding box “person” aparece con score > 0.4.
- Se dispara evento cuando t_obj (en térmica) > T_amb + ΔT (5 C por defecto, 3 C si hum > 80%).
- Forzar caso negativo:
- Coloca cartón con foto de persona (solo RGB) sin firma térmica → no debe disparar.
- Objeto caliente (bolsa de agua caliente) sin detección “person” → no debe disparar.
Criterio: eventos solo con simultaneidad de condiciones; falsos positivos marcadamente inferiores a pipeline solo-RGB.
Verificación 6: Métricas de sistema
- tegrastats:
- GR3D_FREQ ~ 30–70% durante inferencia sostenida.
- Memoria estable; sin OOM.
- Latencia de evento:
- Añade timestamps o usa time.perf_counter() entre infer y log del evento (ya integrado).
- Medir ≤ 150 ms.
Criterio: sistema estable y dentro del presupuesto térmico.
Troubleshooting
1) nvargus “No cameras available” o timeout:
– Revisa conexión del cable CSI (orientación y sujeción).
– Reinicia demonio: sudo systemctl restart nvargus-daemon
– Ejecuta dmesg | grep -i imx para ver si el driver IMX477 se cargó.
– Comprueba que no haya otra app usando la cámara (cierra pipelines GStreamer previos).
2) i2cdetect no muestra 0x33/0x76:
– Verifica cableado SDA/SCL y GND/3V3.
– Cambia la frecuencia del bus a 100 kHz si hay problemas de integridad (en código: I2C a 100000).
– Para BME680 en 0x77, cambia BME680_ADDR = 0x77 en el código.
– Asegúrate de pertenecer al grupo i2c (logout/login tras sudo usermod -aG i2c $USER).
3) Error al construir/trabajar con el engine TensorRT:
– Engine “incompatible” tras actualizar L4T: reconstruye con trtexec en el propio dispositivo (los engines no son portables entre versiones).
– Si –shapes falla por nombre de input, inspecciona el ONNX: onnxsim/netron; ajusta –shapes=
– Si sin memoria (–workspace), reduce workspace o usa –optShapes/–minShapes/–maxShapes aunque sea batch=1.
4) FPS bajo (< 20 FPS):
– Asegúrate de modo MAXN y jetson_clocks activos.
– Cierra overlays/ventanas que consumen CPU.
– Evita conversiones innecesarias; mantén el preprocesado ligero (ya está optimizado).
– Verifica que sea FP16 (–fp16) y no FP32.
– Reduce resolución de captura a 1280×720 o menor; input del modelo es 640×640.
5) MLX90640 lecturas erráticas (NaN o saltos):
– Reduce refresh_rate a 4 Hz; incrementa tiempo entre lecturas.
– Asegura buena alimentación a 3V3 y cables cortos.
– Evita fuentes de calor IR directas saturando el sensor.
6) BME680 reporta humedad/temperatura irreales:
– Evita proximidad a disipador del Jetson (calienta el aire local).
– Añade “offset” de temperatura si instalas cerca de elementos calientes; o aléjalo unos cm.
– Comprueba dirección I2C y el chip (BME688 vs BME680 pueden necesitar librerías distintas).
7) Fallo GStreamer/OpenCV (“can’t open pipeline”):
– Instala gstreamer1.0-plugins-bad/libav.
– Verifica que el string de pipeline sea exactamente el especificado (comillas y caps).
– Usa gst-launch-1.0 para depurar segmento a segmento.
8) PyCUDA error de compilación en instalación:
– Asegúrate de tener python3-dev y build-essential instalados.
– Verifica que CUDA 11.4 esté en /usr/local/cuda; si no, exporta CUDA_HOME=/usr/local/cuda y reinstala pycuda.
– Como alternativa, usa el intérprete del sistema (no en venv) para heredar rutas de CUDA del JetPack.
Mejoras/variantes
- INT8 y calibración: genera engine INT8 con trtexec y dataset de calibración propio para ganar ~1.3–1.7× en FPS manteniendo precisión adecuada.
- Homografía RGB↔térmico: calibra extrínsecos y FOV para mapear con precisión bbox RGB al plano térmico (usa un patrón térmico para registro).
- Multi-ROI y reglas: define varias “líneas” perimetrales con distinta sensibilidad y lógica (conteo, dirección).
- Publicación de eventos: envía JSON vía MQTT/HTTP con métricas (bbox, score, T_obj, T_amb, humedad), e integra con un dashboard (Grafana/InfluxDB).
- DeepStream pipeline: migra a nvinfer + nvtracker si requieres múltiples flujos o visualización acelerada; este caso mantuvo Python+TRT para facilitar fusión de sensores I2C.
- Gestión de energía: alterna nvpmodel según horario; baja FPS nocturno si no hay movimiento (detección de movimiento simple).
Checklist de verificación
- [ ] El sistema ejecuta Ubuntu 20.04.6 + JetPack 5.1.2 (L4T 35.4.1).
- [ ] TensorRT 8.5.x disponible y trtexec funcional.
- [ ] IMX477 detectada por nvargus; pipeline GStreamer sin errores.
- [ ] MLX90640 (0x33) y BME680 (0x76/0x77) visibles en i2cdetect.
- [ ] sensor_check.py arroja valores coherentes (T/H/IR).
- [ ] Motor FP16 generado: yolov5s_fp16.engine presente y cargable.
- [ ] rgb_thermal_perimeter.py corre con FPS_infer ≥ 25.
- [ ] Eventos de intrusión solo cuando hay “person” + ΔT térmico sobre umbral.
- [ ] tegrastats muestra uso de GPU estable y sin OOM.
- [ ] Modo potencia revertido (opcional): sudo nvpmodel -m 1 al finalizar.
Apéndice: comandos útiles de monitoreo y limpieza
- Consultar modo de potencia:
sudo nvpmodel -q
- Establecer MAXN y fijar clocks (recordatorio):
sudo nvpmodel -m 0
sudo jetson_clocks
- Monitoreo en tiempo real:
sudo tegrastats --interval 1000
- Reiniciar servicio de cámara:
sudo systemctl restart nvargus-daemon
- Revertir a modo de menor consumo al terminar:
sudo nvpmodel -m 1
Con este caso práctico, has implementado un sistema de “rgb-thermal-perimeter-intrusion” completamente operativo en NVIDIA Jetson Orin Nano 8GB usando Arducam IMX477, MLX90640 y BME680, con toolchain JetPack 5.1.2, TensorRT 8.5.2 FP16 y GStreamer/OpenCV, validado con métricas cuantitativas y procedimientos reproducibles end-to-end.
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.




