Caso práctico: Clasificación de madurez frutal en Jetson NX

Caso práctico: Clasificación de madurez frutal en Jetson NX — hero

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

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 sistema 'fruit-ripeness-vision-spectra'?




Pregunta 2: ¿Qué tipo de cámara se utiliza en el sistema?




Pregunta 3: ¿Qué tecnología se utiliza para la lectura espectral?




Pregunta 4: ¿Cuál es la frecuencia de procesamiento esperada para la cámara?




Pregunta 5: ¿Qué métricas se utilizan para la clasificación de madurez?




Pregunta 6: ¿Qué latencia se espera para la lectura de muestras espectrales?




Pregunta 7: ¿Cuál es la principal ventaja de fusionar visión y espectro en este sistema?




Pregunta 8: ¿Cuál es el público objetivo del sistema?




Pregunta 9: ¿Qué se busca minimizar en el retail con este sistema?




Pregunta 10: ¿Qué tipo de decisiones puede tomar el sistema en función de la madurez?




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

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

Sígueme:
Scroll to Top