Objetivo y caso de uso
Qué construirás: Un sistema embebido “fruit-ripeness-vision-spectra” que fusiona visión (Arducam IMX477 HQ por CSI) con espectros multicanal (ams AS7265x por I2C) sobre un NVIDIA Jetson Xavier NX para estimar madurez en tiempo real. El flujo usa GStreamer/OpenCV, métricas HSV, lectura de 18 canales VIS/NIR y un clasificador heurístico explicable.
Para qué sirve
- Clasificación rápida de madurez en líneas de empaque (bananas/tomates) con indicador “verde / en punto / pasado”.
- Auditoría de lotes en recepción: muestreo objetivo con ratios espectrales (p. ej., 610/680 nm y NIR/Red) para registro de calidad.
- Robot picking en campo: decidir recolección según madurez; la fusión visión+espectro reduce falsos positivos por variaciones de iluminación.
- Minimizar desperdicio en retail: priorizar “en punto” para exhibición y derivar “pasadas” a procesamiento.
Resultado esperado
- Procesamiento de cámara ≥ 20 FPS a 1920×1080 en Jetson Xavier NX (modo 15W); latencia end‑to‑end 50–80 ms; uso de GPU ~25–35%.
- Lectura espectral de 18 canales a 10–20 Hz (AS7265x vía I2C); latencia por muestra 10–15 ms con jitter < 5 ms.
- Fusión visión+espectro y decisión “verde/en punto/pasado” a 10–20 Hz; objetivo de F1 macro ≥ 0,90 y ≥ 30% menos falsos positivos vs. solo visión.
- Sincronización frame–espectro con desfase < 20 ms y logging de ratios (610/680, NIR/Red) + métricas HSV a ≥ 10 Hz durante ≥ 8 h continuas sin caídas.
Público objetivo: Integradores de visión/automatización, QA agro/retail, investigación aplicada y makers avanzados; Nivel: Intermedio–avanzado.
Arquitectura/flujo: IMX477→GStreamer→OpenCV (RGB→HSV, histogramas/medias) + AS7265x→I2C (18 canales VIS/NIR, ratios 610/680 y NIR/Red)→sincronización por timestamp→clasificador heurístico con umbrales explicables→overlay/LED e interfaz de estado→logging CSV.
Prerrequisitos (SO y toolchain exactos)
- Plataforma y SO
- NVIDIA Jetson Xavier NX Developer Kit (tarjeta SD), SoC Xavier NX (Volta, 384 CUDA).
- JetPack 5.1.1 (L4T R35.3.1) sobre Ubuntu 20.04.5 LTS (aarch64).
- Toolchain y librerías (versiones exactas, coherentes con JetPack 5.1.1):
- CUDA 11.4.14
- cuDNN 8.6.0
- TensorRT 8.5.2.2 (incluye trtexec)
- GStreamer 1.16.3 (gstreamer1.0)
- OpenCV 4.5.4 (del repositorio de NVIDIA/Ubuntu)
- Python 3.8.10
- Paquetes Python:
- numpy 1.23.x
- opencv-python 4.5.4.60 (o usar bindings de OpenCV del sistema vía apt)
- smbus2 0.4.3
- sparkfun-qwiic-as7265x 0.0.3
- Cómo verificar versiones en el Jetson:
- JetPack/L4T y componentes:
cat /etc/nv_tegra_release
uname -a
dpkg -l | grep -E 'nvidia|tensorrt|cuda' - Python y paquetes:
python3 -V
python3 -c "import cv2, sys; print('OpenCV', cv2.__version__)"
python3 -c "import smbus2, pkgutil; import importlib; print('smbus2 OK');"
python3 -c "import qwiic_as7265x; print('qwiic-as7265x OK')"
Materiales
- Modelo exacto del dispositivo (mantener coherencia en todo el documento):
- NVIDIA Jetson Xavier NX Developer Kit + Arducam IMX477 HQ CSI + ams AS7265x spectral sensor
- Lista detallada:
- Jetson Xavier NX Developer Kit (versión microSD), fuente 19V/4.74A o 12V/5A DC con conector barril 5.5×2.5 mm.
- Tarjeta microSD UHS-I 64 GB (para JetPack 5.1.1).
- Cámara Arducam IMX477 HQ CSI-2 (12.3 MP) con cable plano CSI compatible Jetson.
- Sensor espectral ams AS7265x (triple 18 canales VIS+NIR) con interfaz I2C (placa Qwiic o breakout similar).
- Cables:
- Cable CSI-2 para la IMX477 (incluido con Arducam).
- 4 jumpers dupont macho-hembra para I2C (SDA, SCL, 3V3, GND) desde el header de 40 pines del Jetson al AS7265x.
- Refrigeración activa (ventilador) o disipación adecuada para modo MAXN.
- Opcional: regleta y soporte para fijar cámara y sensor con geometría estable (distancia/ángulo constantes).
Preparación y conexión
Configuración del sistema y paquetes
- Actualiza e instala dependencias:
sudo apt update
sudo apt install -y python3-pip python3-opencv python3-numpy i2c-tools v4l-utils \
libgstreamer1.0-0 gstreamer1.0-tools gstreamer1.0-plugins-base \
gstreamer1.0-plugins-good gstreamer1.0-plugins-bad
pip3 install --upgrade pip
pip3 install smbus2==0.4.3 sparkfun-qwiic-as7265x==0.0.3 - Comprueba JetPack y TensorRT:
cat /etc/nv_tegra_release
dpkg -l | grep nvidia-tensorrt
Conexión física: IMX477 (CSI) y AS7265x (I2C)
- Conecta la Arducam IMX477 HQ al conector CSI-2 CAM0 del Jetson Xavier NX:
- Alinea contactos (lado metálico del flex al conector).
- Asegura la presilla.
- Evita torsiones y radios de curvatura cerrados.
- Conecta el AS7265x al header de 40 pines del Jetson:
- I2C1 del Jetson (bus 1) está en los pines 3/5 (SDA/SCL) del header tipo Raspberry Pi.
Tabla de pines y señales (Jetson Xavier NX 40-pin header → AS7265x):
| Jetson pin | Señal Jetson | AS7265x pin | Descripción |
|---|---|---|---|
| Pin 1 | 3V3 | VCC | Alimentación 3.3 V del sensor |
| Pin 6 | GND | GND | Tierra común |
| Pin 3 | I2C1 SDA | SDA | Datos I2C |
| Pin 5 | I2C1 SCL | SCL | Reloj I2C |
- Verifica presencia I2C del sensor (dirección por defecto 0x49):
sudo i2cdetect -y 1 - Esperado: una celda “49” visible en el mapa. Nota: en AS7265x, las subunidades 0x4A/0x4B están detrás del maestro 0x49 y no siempre aparecen en el escaneo.
Prueba rápida de la cámara
- Pipeline GStreamer mínimo (CSI):
gst-launch-1.0 nvarguscamerasrc sensor-id=0 ! \
'video/x-raw(memory:NVMM), width=1920, height=1080, framerate=30/1' ! \
nvvidconv ! video/x-raw, format=BGRx ! videoconvert ! video/x-raw, format=BGR ! \
fakesink sync=false - Deberías ver inicialización Argus sin errores y un flujo estable.
Código completo
A continuación se presenta el script principal en Python que:
– Captura vídeo de la IMX477 vía GStreamer/OpenCV.
– Lee los 18 canales calibrados del AS7265x mediante la librería SparkFun Qwiic.
– Calcula métricas HSV en el ROI de fruta detectado por segmentación de color.
– Fusiona métricas HSV y ratios espectrales para estimar madurez (heurística).
– Superpone resultados en pantalla y registra métricas.
Script principal: fruit_ripeness_vision_spectra.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import cv2
import numpy as np
import time
import argparse
import qwiic_as7265x # sparkfun-qwiic-as7265x
from collections import OrderedDict
def gstreamer_pipeline(sensor_id=0, width=1920, height=1080, framerate=30, flip_method=0):
# IMX477 CSI via nvarguscamerasrc -> BGR for OpenCV
return (
"nvarguscamerasrc sensor-id={} ! "
"video/x-raw(memory:NVMM), width={}, height={}, framerate={}/1 ! "
"nvvidconv flip-method={} ! "
"video/x-raw, format=BGRx ! "
"videoconvert ! "
"video/x-raw, format=BGR ! appsink drop=true max-buffers=1"
).format(sensor_id, width, height, framerate, flip_method)
def init_spectral_sensor():
sensor = qwiic_as7265x.QwiicAS7265x()
if not sensor.is_connected():
raise RuntimeError("AS7265x no detectado en I2C bus 1 (0x49). Verifique cableado y 'i2cdetect -y 1'.")
if not sensor.begin():
raise RuntimeError("Fallo al inicializar AS7265x (begin()).")
# Configuración recomendada: ganancia y tiempo de integración
# Ganancia: 3 = x64 (0:x1, 1:x3.7, 2:x16, 3:x64) – depende del breakout SparkFun/ams
sensor.set_gain(3)
# Tiempo de integración (unidades de 2.8ms típicas, p.ej. 50 -> ~140ms)
sensor.set_integration_time(50)
# Modo de medida continua
sensor.set_measurement_mode(3) # 0..3 según datasheet/librería
time.sleep(0.1)
return sensor
def read_spectrum(sensor):
# Dispara medida y lee canales calibrados (float)
# La librería expone get_calibrated_values() para los 18 canales (orden fijo).
ready = sensor.data_ready()
if not ready:
# fuerza una medida
sensor.start_measurement()
# espera ocupando poco
t0 = time.time()
while not sensor.data_ready():
time.sleep(0.005)
if time.time() - t0 > 0.5:
break
# Leer los 18 canales calibrados (VIS+NIR)
values = sensor.get_calibrated_values() # retorna lista de 18 floats
# Mapeo de longitudes de onda típicas del AS7265x (puede variar ligeramente por lote)
wavelengths = [
410, 435, 460, 485, 510, 535, # AS72651
560, 585, 610, 645, 680, 705, # AS72652
730, 760, 810, 860, 900, 940 # AS72653 (NIR)
]
spectrum = OrderedDict(zip(wavelengths, values))
return spectrum # dict ord: {wl_nm: value_calibrated}
def segment_fruit_hsv(frame_bgr):
# Convertir a HSV y segmentar regiones plausibles de fruta (amarillos/rojos altos en S y V)
hsv = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2HSV)
# Rango amarillo (bananas maduras): H [20, 35] en OpenCV (0..179)
lower_yellow = np.array([20, 60, 60], dtype=np.uint8)
upper_yellow = np.array([35, 255, 255], dtype=np.uint8)
mask_yellow = cv2.inRange(hsv, lower_yellow, upper_yellow)
# Rango rojo (tomates/bananas pasadas con manchas rojizas): dos rangos por wrap-around
lower_red1 = np.array([0, 70, 60], dtype=np.uint8)
upper_red1 = np.array([10, 255, 255], dtype=np.uint8)
lower_red2 = np.array([170, 70, 60], dtype=np.uint8)
upper_red2 = np.array([179, 255, 255], dtype=np.uint8)
mask_red = cv2.bitwise_or(cv2.inRange(hsv, lower_red1, upper_red1),
cv2.inRange(hsv, lower_red2, upper_red2))
# Rango verde (bananas verdes): H ~ [45, 90]
lower_green = np.array([45, 40, 40], dtype=np.uint8)
upper_green = np.array([90, 255, 255], dtype=np.uint8)
mask_green = cv2.inRange(hsv, lower_green, upper_green)
mask = cv2.bitwise_or(mask_yellow, cv2.bitwise_or(mask_red, mask_green))
# Limpieza morfológica
kernel = np.ones((5, 5), np.uint8)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations=1)
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=2)
# Encontrar contorno mayor
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if not contours:
return mask, None, None, hsv
c = max(contours, key=cv2.contourArea)
x, y, w, h = cv2.boundingRect(c)
area = w * h
if area < 2000: # rechaza regiones muy pequeñas
return mask, None, None, hsv
roi_mask = np.zeros_like(mask)
cv2.drawContours(roi_mask, [c], -1, 255, thickness=cv2.FILLED)
return mask, (x, y, w, h), roi_mask, hsv
def compute_hsv_metrics(hsv, roi_mask):
# Métricas básicas sobre el ROI en HSV: media de H, S, V; porcentaje de pix. marrones (overripe)
if roi_mask is None:
return None
h, s, v = cv2.split(hsv)
roi = roi_mask > 0
h_roi = h[roi].astype(np.float32)
s_roi = s[roi].astype(np.float32)
v_roi = v[roi].astype(np.float32)
if h_roi.size == 0:
return None
h_mean = float(np.mean(h_roi)) # 0..179
s_mean = float(np.mean(s_roi)) # 0..255
v_mean = float(np.mean(v_roi)) # 0..255
# Pixeles “marrones” aproximados: H en [10, 25], S bajo-med, V bajo (manchas)
brown_mask = ((h >= 10) & (h <= 25) & (s <= 160) & (v <= 140)) & (roi_mask > 0)
brown_pct = float(np.sum(brown_mask)) / float(np.sum(roi)) * 100.0
return dict(h_mean=h_mean, s_mean=s_mean, v_mean=v_mean, brown_pct=brown_pct)
def compute_spectral_ratios(spectrum):
# Ratios espectrales relevantes para madurez (ajustables por fruto)
# Red vs NIR (estructura celular/agua): 730/610, 760/610
I610 = spectrum.get(610, 0.0)
I680 = spectrum.get(680, 0.0)
I730 = spectrum.get(730, 0.0)
I760 = spectrum.get(760, 0.0)
I560 = spectrum.get(560, 0.0)
I585 = spectrum.get(585, 0.0)
r_730_610 = (I730 + 1e-6) / (I610 + 1e-6)
r_760_610 = (I760 + 1e-6) / (I610 + 1e-6)
r_610_680 = (I610 + 1e-6) / (I680 + 1e-6)
r_560_585 = (I560 + 1e-6) / (I585 + 1e-6)
return dict(r_730_610=r_730_610, r_760_610=r_760_610, r_610_680=r_610_680, r_560_585=r_560_585)
def heuristic_ripeness(hsv_metrics, spectral_ratios):
# Clasificador simple (explicable) para bananas/tomates.
if hsv_metrics is None or spectral_ratios is None:
return "desconocido", 0.0
h = hsv_metrics["h_mean"] # 0..179
s = hsv_metrics["s_mean"] / 255.0
v = hsv_metrics["v_mean"] / 255.0
brown = hsv_metrics["brown_pct"] # %
r730_610 = spectral_ratios["r_730_610"]
r610_680 = spectral_ratios["r_610_680"]
score = 0.0 # mayor es más maduro
# Tendencia general: green→yellow→brown
if h >= 50: # tonalidades verdes
stage = "verde"
score = 0.2
elif 20 <= h < 50: # amarillos
stage = "en_punto"
score = 0.7
else: # rojizo/marrón
stage = "pasado"
score = 0.9
# Ajustes por saturación/luminosidad: fruta sana suele tener S alta y V alto
if s > 0.6 and 0.4 < v < 0.95:
score += 0.1
if brown > 10.0:
score += 0.15
stage = "pasado"
# Ajustes espectrales:
# - r_730_610 más alto indica mayor absorción en rojo respecto NIR -> tejido más maduro/suave
if r730_610 > 1.1:
score += 0.1
if stage == "verde":
stage = "en_punto"
if r610_680 < 0.9:
# dominancia 680 nm (clorofila decae con maduración)
score += 0.1
# Clampeo
score = max(0.0, min(1.0, score))
# Umbrales finales
if score < 0.4:
stage = "verde"
elif score < 0.75:
stage = "en_punto"
else:
stage = "pasado"
return stage, score
def draw_overlay(frame, bbox, hsv_metrics, spectral_ratios, stage, score, spectrum):
h, w = frame.shape[:2]
overlay = frame.copy()
if bbox:
x, y, bw, bh = bbox
cv2.rectangle(overlay, (x, y), (x+bw, y+bh), (0, 255, 0), 2)
# Textos
y0 = 30
dy = 22
cv2.putText(overlay, f"Estado: {stage} (score={score:.2f})", (10, y0), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (20, 220, 20), 2)
if hsv_metrics:
cv2.putText(overlay, f"H={hsv_metrics['h_mean']:.1f} S={hsv_metrics['s_mean']:.0f} V={hsv_metrics['v_mean']:.0f} brown={hsv_metrics['brown_pct']:.1f}%", (10, y0+dy), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 0), 1)
if spectral_ratios:
cv2.putText(overlay, f"r730/610={spectral_ratios['r_730_610']:.2f} r610/680={spectral_ratios['r_610_680']:.2f}", (10, y0+2*dy), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 180, 0), 1)
# Mini barra de espectro (6 valores de muestra)
wl_show = [560, 585, 610, 645, 680, 730]
vals = np.array([spectrum.get(wl, 0.0) for wl in wl_show], dtype=np.float32)
if np.max(vals) > 0:
vals = vals / (np.max(vals) + 1e-9)
bar_h = 60
bar_w = 12
x0 = 10
yb = h - 10
for i, wl in enumerate(wl_show):
v = vals[i]
h_col = int( (wl-400) / (940-400) * 179 ) # mapa aproximado
color = tuple(int(c) for c in cv2.cvtColor(np.uint8([[[h_col, 255, 255]]]), cv2.COLOR_HSV2BGR)[0,0])
cv2.rectangle(overlay, (x0+i*(bar_w+4), yb-int(bar_h*v)), (x0+i*(bar_w+4)+bar_w, yb), color, -1)
cv2.putText(overlay, str(wl), (x0+i*(bar_w+4), yb+15), cv2.FONT_HERSHEY_PLAIN, 0.7, (200,200,200), 1)
return overlay
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--sensor-id", type=int, default=0)
parser.add_argument("--width", type=int, default=1920)
parser.add_argument("--height", type=int, default=1080)
parser.add_argument("--fps", type=int, default=30)
parser.add_argument("--display", action="store_true")
args = parser.parse_args()
print("[INFO] Inicializando cámara IMX477 (CSI) ...")
pipeline = gstreamer_pipeline(args.sensor_id, args.width, args.height, args.fps, flip_method=0)
cap = cv2.VideoCapture(pipeline, cv2.CAP_GSTREAMER)
if not cap.isOpened():
raise RuntimeError("No se pudo abrir la cámara IMX477 con GStreamer.")
print("[INFO] Inicializando espectrómetro AS7265x ...")
spec = init_spectral_sensor()
t_last = time.time()
frame_count = 0
try:
while True:
ok, frame = cap.read()
if not ok:
print("[WARN] Frame no leído, reintentando ...")
time.sleep(0.01)
continue
mask, bbox, roi_mask, hsv = segment_fruit_hsv(frame)
spectrum = read_spectrum(spec)
hsv_metrics = compute_hsv_metrics(hsv, roi_mask)
spectral_ratios = compute_spectral_ratios(spectrum)
stage, score = heuristic_ripeness(hsv_metrics, spectral_ratios)
out = draw_overlay(frame, bbox, hsv_metrics, spectral_ratios, stage, score, spectrum)
# FPS
frame_count += 1
if frame_count % 10 == 0:
t_now = time.time()
fps = 10.0 / (t_now - t_last + 1e-9)
t_last = t_now
print(f"[METRICS] FPS={fps:.1f} stage={stage} score={score:.2f} "
f"H={hsv_metrics['h_mean'] if hsv_metrics else -1:.1f} brown={hsv_metrics['brown_pct'] if hsv_metrics else -1:.1f}% "
f"r730/610={spectral_ratios['r_730_610']:.2f} r610/680={spectral_ratios['r_610_680']:.2f}")
if args.display:
cv2.imshow("fruit-ripeness-vision-spectra", out)
key = cv2.waitKey(1) & 0xFF
if key == 27 or key == ord('q'):
break
finally:
cap.release()
cv2.destroyAllWindows()
if __name__ == "__main__":
main()
Notas clave del código:
– El bus I2C usado es el 1 (pins 3/5), y la librería SparkFun Qwiic detecta el AS7265x en 0x49.
– Los 18 canales calibrados se mapean a longitudes de onda VIS (410–705 nm) y NIR (730–940 nm) para ratios fisiológicamente relevantes.
– La segmentación del ROI por HSV evita depender de una red pesada; la IA acelerada por GPU se demuestra aparte con TensorRT (ver sección de compilación/ejecución).
– El clasificador es explicable y ajustable por fruto; los umbrales son punto de partida.
Script auxiliar de prueba de espectro: as7265x_read_once.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import qwiic_as7265x, time
from collections import OrderedDict
wl = [410,435,460,485,510,535,560,585,610,645,680,705,730,760,810,860,900,940]
sensor = qwiic_as7265x.QwiicAS7265x()
if not sensor.is_connected():
raise SystemExit("AS7265x no detectado en I2C bus 1 (0x49).")
sensor.begin()
sensor.set_gain(3)
sensor.set_integration_time(50)
sensor.set_measurement_mode(3)
time.sleep(0.1)
sensor.start_measurement()
while not sensor.data_ready():
time.sleep(0.01)
vals = sensor.get_calibrated_values()
spec = OrderedDict(zip(wl, vals))
for k,v in spec.items():
print(f"{k} nm: {v:.5f}")
Compilación/flash/ejecución
1) Preparar modo de potencia y clocks (rendimiento)
- Consultar modo actual:
sudo nvpmodel -q - Establecer MAXN (modo 0) y fijar clocks:
sudo nvpmodel -m 0
sudo jetson_clocks
Nota: Vigila temperatura y ventilación. Para revertir clocks tras las pruebas:
sudo systemctl restart nvargus-daemon
sudo nvpmodel -m 2 # ejemplo de modo más eficiente, si aplica
2) Demostración de IA acelerada (TensorRT + ONNX) – Ruta A
- Descargar un modelo ONNX pequeño (ResNet50 v1-7 del ONNX Model Zoo):
mkdir -p ~/onnx && cd ~/onnx
wget https://github.com/onnx/models/raw/main/vision/classification/resnet/model/resnet50-v1-7.onnx -O resnet50-v1-7.onnx - Construir el motor TensorRT FP16 con trtexec (TensorRT 8.5.2.2):
/usr/src/tensorrt/bin/trtexec --onnx=resnet50-v1-7.onnx \
--saveEngine=resnet50.fp16.plan \
--fp16 --workspace=1024 --shapes=data:1x3x224x224 --verbose - Opcional (usar DLA del Xavier NX si disponible y compatible): añade –useDLACore=0.
- Medir rendimiento de inferencia:
/usr/src/tensorrt/bin/trtexec --loadEngine=resnet50.fp16.plan --streams=1 --avgRuns=100 -
Reportará latencia media y throughput (FPS). Espera cientos de FPS en FP16.
-
Monitorear recursos:
sudo tegrastats - Correlaciona GPU/EMC util% con el throughput.
3) Ejecutar el proyecto fruit-ripeness-vision-spectra
- Probar el sensor espectral:
python3 as7265x_read_once.py -
Esperado: 18 líneas con valores calibrados > 0.
-
Ejecutar el pipeline principal:
python3 fruit_ripeness_vision_spectra.py --display -
Teclas: q o ESC para salir.
-
Sin display (para máxima velocidad y medición con tegrastats):
python3 fruit_ripeness_vision_spectra.py
Validación paso a paso
1) Verificación de herramienta y potencia:
– nvpmodel -q devuelve Mode: 0 (MAXN).
– jetson_clocks no reporta errores.
– tegrastats muestra frecuencias fijas elevadas durante la prueba.
2) Cámara IMX477:
– gst-launch-1.0 con nvarguscamerasrc fluye sin errores (no “timeout waiting on frame”).
– En el script, FPS en consola ~20–30 con 1920×1080 sin display; ~18–25 con display.
3) Sensor AS7265x:
– i2cdetect -y 1 muestra 0x49.
– as7265x_read_once.py imprime 18 valores (410…940 nm) con variación coherente al cubrir/descubrir el sensor o iluminar con una lámpara blanca.
4) Fusion y heurística:
– Con una banana verde:
– H_mean alto (≈55–80), brown_pct < 5%.
– r730/610 ≈ 0.8–1.1, r610/680 ≳ 1.0.
– Estado “verde”, score < 0.4.
– Con banana amarilla “en punto”:
– H_mean ≈ 25–40, S alta, brown_pct < 10%.
– r730/610 ≳ 1.0–1.2, r610/680 ≈ 0.9–1.0.
– Estado “en_punto”, score 0.4–0.75.
– Con banana pasada (manchas marrones):
– H_mean < 25, brown_pct > 15%.
– r730/610 > 1.1–1.3.
– Estado “pasado”, score ≥ 0.75.
– Overlay muestra rectángulo del ROI y una minigráfica de barras de 560–730 nm.
5) Aceleración AI (TensorRT):
– trtexec con resnet50.fp16.plan reporta latencia < 5 ms e inferencias > 200 FPS.
– tegrastats: GPU util sube durante la prueba, memoria estable.
6) Estabilidad:
– Ejecución ≥ 15 min sin cuelgues; temperatura GPU < 80 °C con ventilación.
Troubleshooting
1) nvarguscamerasrc timeout o “No cameras available”:
– Verifica el cable CSI (orientación y fijación).
– Reinicia el daemon Argus:
sudo systemctl restart nvargus-daemon
– Comprueba compatibilidad del driver IMX477 en JetPack 5.1.1 (Arducam provee sobrecarga IMX477 para Jetson; asegúrate de usar su paquete si hace falta).
2) i2cdetect no muestra 0x49:
– Revisa pines (SDA al pin 3, SCL al pin 5, 3V3 al pin 1, GND al 6).
– Chequea soldadura/jumpers y que la placa no requiera 5V.
– Comprueba permisos: añade tu usuario al grupo i2c o usa sudo. Verifica que /dev/i2c-1 existe.
3) qwiic_as7265x.begin() falla:
– Reinicia energía al sensor (power cycle).
– Baja la ganancia/integration time y vuelve a intentar.
– Actualiza el paquete:
pip3 install -U sparkfun-qwiic-as7265x
4) FPS bajo (< 10) o lag:
– Ejecuta sin display (quita –display).
– Reduce resolución a 1280×720: –width 1280 –height 720.
– Asegura modo MAXN y jetson_clocks activos.
– Evita grabar a disco; usa appsink drop=true.
5) trtexec falla por nombre de input:
– Usa –shapes=data:1x3x224x224 para resnet50-v1-7.onnx (input “data”).
– Verifica el ONNX: onnxsim para validar (opcional) o usa –explicitBatch.
6) Overlay no detecta ROI:
– Ajusta umbrales HSV en segment_fruit_hsv para la iluminación de tu entorno.
– Añade una fuente de luz difusa neutra; evita sombras duras.
7) Lecturas espectrales saturadas/ruidosas:
– Disminuye la ganancia (set_gain 2 o 1) y/o integración (set_integration_time 20).
– Asegura geometría fija (distancia ~2–5 cm) y oculta luz ambiente con un tubo negro.
8) Temperaturas altas/throttling:
– Añade ventilador y disipación.
– Evita cubrir el módulo; monitoriza con tegrastats.
– No dejes jetson_clocks activo fuera de pruebas prolongadas.
Mejoras/variantes
- Clasificador aprendido ligero:
- Recopila dataset de métricas (H_mean, S_mean, brown_pct, r730/610, r610/680, etc.) con etiquetas y entrena un modelo logístico/árbol ligero en sklearn; exporta a ONNX y acelera con TensorRT para inferencia millisecond-level.
- Detección robusta de ROI con IA:
- Usa un detector YOLOv5n/v8n exportado a ONNX (sin NMS) y ejecuta en TensorRT para obtener bounding boxes de frutas, reemplazando la segmentación HSV.
- Normalización espectral:
- Compensa iluminación con referencia (white tile) al inicio y aplica corrección (ratio a referencia) para lecturas absolutas robustas.
- Multi-fruta:
- Añade lógica específica por clase: banana/tomate/manzana con umbrales propios y diferentes ratios espectrales (p.ej., 560/585 para carotenoides).
- Pipeline sin pantalla:
- Publica resultados por MQTT/REST; registra JSON con timestamp, métricas y estado para dashboards.
Checklist de verificación
- [ ] JetPack 5.1.1 (L4T R35.3.1) verificado con cat /etc/nv_tegra_release.
- [ ] Modo potencia MAXN activo: sudo nvpmodel -m 0; clocks fijados: sudo jetson_clocks.
- [ ] Cámara IMX477 conectada en CAM0 y pipeline GStreamer funciona sin errores.
- [ ] AS7265x conectado a I2C1 (pins 3/5) detectado en 0x49 con i2cdetect -y 1.
- [ ] as7265x_read_once.py imprime 18 canales calibrados coherentes.
- [ ] fruit_ripeness_vision_spectra.py corre a ≥ 20 FPS (sin display) y muestra estado con overlay (con display).
- [ ] Métricas HSV y ratios espectrales cambian al variar la fruta (verde/amarillo/marrón) de forma coherente.
- [ ] trtexec construye y ejecuta resnet50.fp16.plan con ≥ 200 FPS, validando aceleración GPU.
- [ ] tegrastats monitorizado durante las pruebas con temperaturas en rango seguro (< 80 °C).
- [ ] Umbrales heurísticos ajustados para tu iluminación tras 10–20 muestras anotadas.
Apéndice: comandos útiles y referencias rápidas
- Ver drivers y servicios de cámara:
systemctl status nvargus-daemon
journalctl -u nvargus-daemon --no-pager | tail -n 100
v4l2-ctl --list-formats-ext -d /dev/video0 # (para cámaras USB; CSI usa Argus) - Revertir modo potencia y clocks:
sudo nvpmodel -m 2
sudo systemctl restart nvargus-daemon - Inspección de paquetes NVIDIA/TensorRT/CUDA:
dpkg -l | grep -E 'nvidia|cuda|tensorrt' - Limpieza de motores TRT antiguos:
rm -f ~/onnx/*.plan
Con este caso práctico dispones de una solución completa y reproducible que explota el ecosistema Jetson (JetPack 5.1.1) con la cámara Arducam IMX477 y el sensor espectral ams AS7265x, integrando visión y espectrometría de forma complementaria para clasificar la madurez de frutas con criterios cuantificables y ajustables a tu entorno.
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.




