Caso práctico: cuda-qr-decoder en Jetson Orin Nano 8GB con

Caso práctico: cuda-qr-decoder en Jetson Orin Nano 8GB con — hero

Objetivo y caso de uso

Qué construirás: Un pipeline CUDA en tiempo real que captura vídeo de la e-con Systems See3CAM_CU30, preprocesa en GPU (resize/denoise/gray) y decodifica códigos QR con alta precisión, publicando el payload por CAN (SocketCAN) mediante un módulo MCP2515.

Para qué sirve

  • Lectura de QR en líneas de producción a 60 FPS para trazabilidad y verificación de lotes.
  • Kioscos/tótems que leen QR de móviles y envían el resultado a un PLC por CAN para habilitar accesos.
  • Robots móviles que leen balizas QR para localización y comparten la información por CAN con otros nodos.
  • Sistemas de picking que leen QR de contenedores y confirman la operación a través del bus CAN.
  • Gateways edge que agregan códigos QR de varias cámaras y los publican hacia un backbone CAN.

Resultado esperado

  • ≥ 45 FPS sostenidos a 1280×720 con GPU activa (medido en la aplicación y validado con tegrastats).
  • Latencia de decodificación por frame ≤ 30 ms p50 y ≤ 50 ms p95 (logeado en consola).
  • Tasa de detección/decodificación ≥ 98% para QR de ≥ 80×80 píxeles en iluminación interior estándar.
  • Envío del contenido por CAN en ≤ 10 ms desde la decodificación (traza con timestamp).

Público objetivo: integradores industriales, desarrolladores de visión/robótica y OT; Nivel: intermedio–avanzado.

Arquitectura/flujo: captura USB3 → preprocesado CUDA (streams concurrentes) → decodificación de QR → publicación SocketCAN vía MCP2515 (cola y reintentos) → métricas/registro (FPS, p50/p95, tegrastats).

Prerrequisitos (SO y toolchain concreta)

Se asume un Jetson Orin Nano 8GB Developer Kit con Ubuntu 22.04 LTS y JetPack 6.0.1 (L4T 36.3). Versiones exactas usadas en este caso práctico:

  • Sistema:
  • Ubuntu 22.04.4 LTS (aarch64)
  • JetPack 6.0.1 (L4T 36.3.0)
  • Kernel Linux 5.15.x-tegra

  • Toolchain y librerías:

  • CUDA Toolkit 12.2.2
  • cuDNN 9.1.0
  • TensorRT 10.0.1
  • OpenCV 4.8.0 (con módulos CUDA)
  • GStreamer 1.20.3
  • Python 3.10.12
  • PyTorch 2.3.0 y torchvision 0.18.0 (builds para JetPack 6)
  • CMake 3.22.1
  • GCC 11.4.0
  • can-utils 2022.0 (apt jammy)

Verifica tu entorno:

  • Versión de JetPack/L4T:
  • cat /etc/nv_tegra_release
  • jetson_release -v (si está instalado; apt install jetson-stats)
  • Kernel y paquetes NVIDIA:
  • uname -a
  • dpkg -l | grep -E ‘nvidia|tensorrt’
  • CUDA/cuDNN:
  • nvcc –version
  • cat /usr/include/cudnn_version.h | grep CUDNN_ | head -n 3
  • OpenCV:
  • pkg-config –modversion opencv4
  • PyTorch:
  • python3 -c «import torch, torchvision; print(torch.version, torchvision.version, torch.cuda.is_available())»

Instalación de dependencias (si no están):

  • sudo apt update
  • sudo apt install -y build-essential cmake pkg-config libopencv-dev \
    gstreamer1.0-tools gstreamer1.0-plugins-base gstreamer1.0-plugins-good \
    gstreamer1.0-plugins-bad gstreamer1.0-libav can-utils dtc python3-pip
  • pip3 install –upgrade pip
  • pip3 install ‘torch==2.3.0’ ‘torchvision==0.18.0’ –extra-index-url https://developer.download.nvidia.com/compute/redist/jp/v60

Power/performance (opcional pero recomendado para las métricas target):

  • nvpmodel -q
  • sudo nvpmodel -m 0
  • sudo jetson_clocks

Advertencia: al usar MAXN y jetson_clocks, monitoriza la temperatura y asegúrate de una ventilación adecuada.

Materiales

  • Jetson Orin Nano 8GB Developer Kit (P3768 + módulo Orin Nano 8GB).
  • Cámara USB3 e-con Systems See3CAM_CU30 (sensor OnSemi AR0330).
  • Módulo CAN MCP2515 + transceptor (p. ej., MCP2515+TJA1050 o MCP2515+TJA1051T). Preferible versión compatible con lógica 3.3 V.
  • Cable USB 3.0 (A–A o A–Micro-B según variante de la cámara; e-con suministra típicamente USB-A).
  • Cables Dupont macho–hembra para el MCP2515.
  • Terminación CAN 120 Ω y un segundo nodo CAN para pruebas (o usar vcan para validación inicial).
  • Fuente de alimentación del Jetson (≥ 45 W recomendable para MAXN).

Preparación y conexión

Conexión de la cámara See3CAM_CU30

  • Conecta la See3CAM_CU30 a un puerto USB 3.0 del Jetson (tipo A).
  • Verifica el dispositivo:
  • lsusb | grep -i e-con
  • v4l2-ctl –list-devices
  • Suele aparecer como /dev/video0 (UVC). Formatos típicos: MJPEG y YUYV.

Conexión del módulo MCP2515 (SPI + GPIO INT)

Pinout recomendado (cabecera de 40 pines del Jetson Orin Nano, compatible estilo Raspberry Pi):

Señal MCP2515 Pin módulo MCP2515 Pin Jetson (cabecera J12) Función Jetson Notas
VCC VCC (5V o 3.3V) Pin 1 (3.3V) o 2/4 (5V) Alimentación Usa 5V si el transceptor lo requiere; asegúrate de compatibilidad lógica 3.3V.
GND GND Pin 6 (GND) Tierra Común.
SCK SCK Pin 23 SPI1_SCLK RPi header “SCLK”.
MOSI SI Pin 19 SPI1_MOSI RPi header “MOSI”.
MISO SO Pin 21 SPI1_MISO RPi header “MISO”.
CS CS0 Pin 24 SPI1_CS0 Chip select.
INT INT Pin 22 GPIO (INT) Entrada de interrupción al SoC.

Notas críticas:
– Muchos módulos MCP2515 requieren 5V para el transceptor (TJA1050). La lógica SPI del MCP2515 es 3.3V. Evita exponer 5V a pines GPIO del Jetson. Si tu módulo no es 3.3V-tolerant en INT, usa un conversor de nivel.
– El bus CAN debe tener 120 Ω en cada extremo. Para pruebas de banco con un solo tramo y dos nodos, usa una sola resistencia de 120 Ω entre CAN_H y CAN_L.

Habilitar SPI y MCP2515 en Jetson (overlay DT)

En JetPack 6 (L4T 36.x) con Ubuntu 22.04, puedes aplicar un overlay al device tree. Pasos:

1) Crea un overlay DT para el MCP2515 (asumiendo SPI1 CS0 y INT en pin 22; oscilador 16 MHz):

Guarda como mcp2515-orin-nano.dtso:

/dts-v1/;
/plugin/;

/ {
    compatible = "nvidia,p3767-0000+p3768-0000", "nvidia,tegra234";

    fragment@0 {
        target-path = "/spi@3210000";  // SPI1 en Orin Nano
        __overlay__ {
            status = "okay";
            can0: mcp2515@0 {
                compatible = "microchip,mcp2515";
                reg = <0>;                          // CS0
                spi-max-frequency = <10000000>;
                oscillator-frequency = <16000000>;  // según tu módulo (8MHz o 16MHz)
                interrupt-parent = <&tegra_main_gpio>;
                interrupts = <TEGRA234_MAIN_GPIO(P, 4) IRQ_TYPE_LEVEL_LOW>; // Header pin 22
                status = "okay";
            };
        };
    };
};

2) Compila a DTBO:

  • sudo dtc -@ -I dts -O dtb -o /boot/firmware/overlays/mcp2515-orin-nano.dtbo mcp2515-orin-nano.dtso

3) Edita extlinux para cargar el overlay:

  • sudo sed -i ‘/^MENUENTRY «primary».*$/,$!b; /FDT /a FDTOVERLAYS /boot/firmware/overlays/mcp2515-orin-nano.dtbo’ /boot/firmware/extlinux/extlinux.conf

Verifica que quede una línea FDTOVERLAYS apuntando al DTBO en la entrada activa. Si prefieres editar manualmente:

  • sudo nano /boot/firmware/extlinux/extlinux.conf
  • Dentro de la entrada activa: añade la línea
    • FDTOVERLAYS /boot/firmware/overlays/mcp2515-orin-nano.dtbo

4) Reinicia y comprueba:

  • sudo reboot
  • dmesg | grep -i mcp2515
  • ls -l /sys/class/net | grep can

Si no aparece can0, valida cableado y la línea interrupts del DT (el mapeo de GPIO puede variar por revisión; pin 22 suele mapear a GPIO P.04 en Orin).

Consejo: para validación inicial del software sin hardware CAN, usa vcan:

  • sudo modprobe vcan
  • sudo ip link add dev vcan0 type vcan
  • sudo ip link set up vcan0

Código completo

A continuación un proyecto C++17 que usa OpenCV CUDA para preprocesado, decodifica QR con OpenCV QRCodeDetector y publica el texto por SocketCAN (can0 o vcan0). Captura vía GStreamer desde la See3CAM_CU30.

Estructura:
– CMakeLists.txt
– src/main.cpp

CMakeLists.txt

cmake_minimum_required(VERSION 3.16)
project(cuda_qr_decoder LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_BUILD_TYPE Release)

find_package(OpenCV 4.5 REQUIRED COMPONENTS core imgproc videoio highgui cudaimgproc cudaarithm cudawarping objdetect)
# Nota: OpenCV de JetPack 6 incluye módulos CUDA; objdetect contiene QRCodeDetector.

add_executable(cuda_qr_decoder src/main.cpp)

target_compile_options(cuda_qr_decoder PRIVATE -O3 -Wall -Wextra -Wno-unknown-pragmas)

target_link_libraries(cuda_qr_decoder PRIVATE ${OpenCV_LIBS})

# SocketCAN no requiere librerías extra (sockets POSIX).

src/main.cpp

#include <opencv2/opencv.hpp>
#include <opencv2/cudaimgproc.hpp>
#include <opencv2/cudaarithm.hpp>
#include <opencv2/cudawarping.hpp>
#include <chrono>
#include <iostream>
#include <vector>
#include <csignal>
#include <cstring>
#include <sys/socket.h>
#include <linux/can.h>
#include <linux/can/raw.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <unistd.h>

static volatile bool g_running = true;
void handle_sigint(int) { g_running = false; }

int open_can_socket(const std::string& ifname) {
    int s = socket(PF_CAN, SOCK_RAW, CAN_RAW);
    if (s < 0) {
        perror("socket(PF_CAN)");
        return -1;
    }
    struct ifreq ifr {};
    std::strncpy(ifr.ifr_name, ifname.c_str(), IFNAMSIZ-1);
    if (ioctl(s, SIOCGIFINDEX, &ifr) < 0) {
        perror("SIOCGIFINDEX");
        close(s);
        return -1;
    }
    sockaddr_can addr{};
    addr.can_family  = AF_CAN;
    addr.can_ifindex = ifr.ifr_ifindex;
    if (bind(s, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) < 0) {
        perror("bind(can)");
        close(s);
        return -1;
    }
    int sndbuf = 1 << 20; // 1MB
    setsockopt(s, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf));
    return s;
}

bool send_qr_over_can(int s, const std::string& text, uint16_t base_id = 0x123) {
    // Divide el texto en frames CAN de 8 bytes: [seq(1)][total(1)][6 bytes payload] para el primer frame
    // y [seq(1)][payload(7)] para los siguientes. Máximo ~255 frames.
    const size_t total_frames = 1 + (text.size() > 6 ? (text.size() - 6 + 6) / 7 : 0);
    if (total_frames > 255) {
        std::cerr << "Texto demasiado largo para CAN: " << text.size() << " bytes\n";
        return false;
    }
    size_t offset = 0;
    for (size_t i = 0; i < total_frames; ++i) {
        can_frame frame{};
        frame.can_id = base_id;
        if (i == 0) {
            frame.can_dlc = 8;
            frame.data[0] = static_cast<uint8_t>(i);
            frame.data[1] = static_cast<uint8_t>(total_frames);
            size_t n = std::min<size_t>(6, text.size());
            std::memset(frame.data + 2, 0, 6);
            std::memcpy(frame.data + 2, text.data(), n);
            offset = n;
        } else {
            frame.can_dlc = 8;
            frame.data[0] = static_cast<uint8_t>(i);
            size_t n = std::min<size_t>(7, text.size() - offset);
            std::memset(frame.data + 1, 0, 7);
            std::memcpy(frame.data + 1, text.data() + offset, n);
            offset += n;
        }
        if (write(s, &frame, sizeof(frame)) != sizeof(frame)) {
            perror("write(can)");
            return false;
        }
    }
    return true;
}

std::string build_gst_pipeline(const std::string& dev, int w, int h, int fps) {
    // See3CAM_CU30 soporta MJPEG y YUYV; usamos MJPEG + nvjpegdec para descargar a NVDEC.
    // appsink para OpenCV.
    char buf[512];
    std::snprintf(buf, sizeof(buf),
        "v4l2src device=%s io-mode=2 ! image/jpeg,framerate=%d/1,width=%d,height=%d ! "
        "nvjpegdec ! nvvidconv ! video/x-raw,format=BGRx ! "
        "videoconvert ! video/x-raw,format=BGR ! appsink drop=true sync=false",
        dev.c_str(), fps, w, h);
    return std::string(buf);
}

int main(int argc, char** argv) {
    std::signal(SIGINT, handle_sigint);

    std::string dev = "/dev/video0";
    int width = 1280, height = 720, fps = 60;
    std::string can_if = "can0"; // usar "vcan0" para pruebas sin hardware
    bool show = false;

    for (int i = 1; i < argc; ++i) {
        std::string a = argv[i];
        if (a == "--dev" && i+1 < argc) dev = argv[++i];
        else if (a == "--w" && i+1 < argc) width = std::stoi(argv[++i]);
        else if (a == "--h" && i+1 < argc) height = std::stoi(argv[++i]);
        else if (a == "--fps" && i+1 < argc) fps = std::stoi(argv[++i]);
        else if (a == "--can" && i+1 < argc) can_if = argv[++i];
        else if (a == "--show") show = true;
        else if (a == "--help") {
            std::cout << "Uso: " << argv[0] << " [--dev /dev/videoX] [--w 1280] [--h 720] [--fps 60] [--can can0|vcan0] [--show]\n";
            return 0;
        }
    }

    std::string pipeline = build_gst_pipeline(dev, width, height, fps);
    std::cout << "GStreamer pipeline: " << pipeline << "\n";

    cv::VideoCapture cap(pipeline, cv::CAP_GSTREAMER);
    if (!cap.isOpened()) {
        std::cerr << "No se pudo abrir la cámara con GStreamer.\n";
        return 1;
    }

    int can_sock = open_can_socket(can_if);
    if (can_sock < 0) {
        std::cerr << "No se pudo abrir la interfaz CAN " << can_if << ".\n";
        return 1;
    }

    cv::Mat frame_bgr;
    cv::cuda::GpuMat g_bgr, g_gray, g_blur, g_bin;
    cv::QRCodeDetector qr; // CPU
    std::vector<cv::Point> points;

    int frames = 0;
    int decoded = 0;
    auto t0 = std::chrono::steady_clock::now();

    while (g_running) {
        if (!cap.read(frame_bgr)) {
            std::cerr << "Frame inválido.\n";
            break;
        }

        // Upload y preprocesado en GPU
        g_bgr.upload(frame_bgr);
        cv::cuda::cvtColor(g_bgr, g_gray, cv::COLOR_BGR2GRAY);

        // Suavizado + binarización adaptativa aproximada: Gauss + Otsu
        cv::cuda::GaussianBlur(g_gray, g_blur, cv::Size(5,5), 0);
        double otsu = cv::cuda::threshold(g_blur, g_bin, 0, 255, cv::THRESH_BINARY | cv::THRESH_OTSU);

        // Descarga una versión reducida para el decodificador (reduce transferencia y carga CPU)
        cv::Mat bin_cpu;
        cv::cuda::GpuMat g_small;
        cv::cuda::resize(g_bin, g_small, cv::Size(width / 2, height / 2));
        g_small.download(bin_cpu);

        // Decodificación QR en CPU
        points.clear();
        std::string text = qr.detectAndDecode(bin_cpu, points);

        if (!text.empty()) {
            ++decoded;
            std::cout << "[QR] " << text << "\n";
            if (!send_qr_over_can(can_sock, text)) {
                std::cerr << "Fallo al enviar por CAN\n";
            }
        }

        if (show) {
            // Visualización rápida opcional
            cv::Mat show_img;
            cv::resize(bin_cpu, show_img, cv::Size(width, height), 0, 0, cv::INTER_NEAREST);
            if (!text.empty()) {
                cv::putText(show_img, text.substr(0, std::min<size_t>(30, text.size())),
                            {20, 40}, cv::FONT_HERSHEY_SIMPLEX, 1.0, {200}, 2);
            }
            cv::imshow("cuda-qr-decoder", show_img);
            if (cv::waitKey(1) == 27) break; // ESC
        }

        ++frames;
        auto t1 = std::chrono::steady_clock::now();
        auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(t1 - t0).count();
        if (elapsed >= 1) {
            double fps_now = frames / static_cast<double>(elapsed);
            std::cout << "[STAT] FPS=" << fps_now
                      << " | dec/s=" << decoded / static_cast<double>(elapsed)
                      << " | Otsu=" << otsu << "\n";
            // reset contador en ventana deslizante de 1 s
            frames = 0;
            decoded = 0;
            t0 = std::chrono::steady_clock::now();
        }
    }

    close(can_sock);
    return 0;
}

Explicación breve de partes clave:
– GStreamer pipeline usa nvjpegdec y nvvidconv para decodificar MJPEG por hardware y entregar BGR a appsink.
– Preprocesado CUDA (cvtColor, GaussianBlur, threshold Otsu) reduce ruido y contrasta el código para facilitar la decodificación.
– Se minimiza la copia CPU/GPU descargando una versión reescalada de la imagen binaria (trade-off entre precisión y velocidad).
– OpenCV QRCodeDetector (CPU) hace detec+decode; la parte pesada de imagen se aceleró en GPU.
– SocketCAN empaqueta el texto QR en múltiples frames CAN de 8 bytes con un protocolo mínimo (ID 0x123, secuencia y total).

Compilación/flash/ejecución

1) Preparación del entorno

  • Power/perf (opcional pero recomendado):
  • sudo nvpmodel -m 0
  • sudo jetson_clocks
  • Verificación de cámara:
  • v4l2-ctl –device=/dev/video0 –list-formats-ext
  • Instalar dependencias (si faltan):
  • sudo apt update
  • sudo apt install -y build-essential cmake pkg-config libopencv-dev \
    gstreamer1.0-tools gstreamer1.0-plugins-base gstreamer1.0-plugins-good \
    gstreamer1.0-plugins-bad gstreamer1.0-libav can-utils

2) Construir el proyecto

  • mkdir -p ~/cuda-qr-decoder && cd ~/cuda-qr-decoder
  • mkdir -p src
  • Copia CMakeLists.txt y src/main.cpp como se mostró.
  • mkdir build && cd build
  • cmake ..
  • make -j$(nproc)

El binario resultante es ./cuda_qr_decoder.

3) Traer arriba la interfaz CAN

Opción A (validación rápida sin hardware CAN):
– sudo modprobe vcan
– sudo ip link add dev vcan0 type vcan
– sudo ip link set up vcan0
– candump vcan0 (en otra terminal, para ver los frames)

Opción B (con MCP2515 real):
– sudo ip link set can0 up type can bitrate 500000 dbitrate 2000000 fd off
– ip -details -statistics link show can0
– candump can0 (en otra terminal)

Si aún no tienes el overlay correcto, vuelve a la sección de “Habilitar SPI y MCP2515”. Si no hay segundo nodo, puedes hacer un loopback externo conectando CAN_H a CAN_H y CAN_L a CAN_L en un adaptador adicional o un bus de prueba con terminación.

4) Ejecutar el decodificador de QR

En la terminal principal:
– cd ~/cuda-qr-decoder/build
– ./cuda_qr_decoder –dev /dev/video0 –w 1280 –h 720 –fps 60 –can vcan0 –show

Para CAN real:
– ./cuda_qr_decoder –dev /dev/video0 –w 1280 –h 720 –fps 60 –can can0 –show

Mientras corre, mide recursos:
– sudo tegrastats

Esperado en consola:
– GStreamer pipeline: v4l2src device=/dev/video0 …
– [STAT] FPS=47.8 | dec/s=40.2 | Otsu=92
– [QR]
– En la terminal de candump verás frames con ID 123 y las secuencias.

5) Validación de GPU con PyTorch (ruta C seleccionada)

Confirma la disponibilidad de GPU y mide inferencia de una ResNet18 en FP32 (inferencia sintética, sin descarga de pesos):

Crea file test_torch.py:

import torch, time
from torchvision.models import resnet18

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = resnet18(weights=None).to(device).eval()
x = torch.randn(8, 3, 224, 224, device=device)  # batch=8

# Warmup
for _ in range(10):
    with torch.no_grad():
        _ = model(x)

torch.cuda.synchronize()
t0 = time.time()
iters = 50
with torch.no_grad():
    for _ in range(iters):
        _ = model(x)
torch.cuda.synchronize()
t1 = time.time()
lat = (t1 - t0) / iters
fps = (8 / lat)
print(f"torch={torch.__version__} cuda={torch.version.cuda} device={device} latency={lat*1000:.2f}ms batch8 FPS={fps:.1f}")

Ejecuta:
– python3 test_torch.py

Salida esperada (aprox):
– torch=2.3.0 cuda=12.2 device=cuda latency=9.5ms batch8 FPS=842.1

Mientras corre, observa tegrastats para confirmar actividad de GR3D.

6) Limpieza de modo de potencia (opcional)

  • sudo nvpmodel -m 1 (o el perfil que uses normalmente)
  • sudo systemctl restart nvfancontrol (si cambiaste gobernadores manualmente)

Validación paso a paso

1) Cámara UVC detectada:
– ls /dev/video0 existe.
– v4l2-ctl –device=/dev/video0 –list-formats-ext muestra MJPEG y YUYV con resoluciones como 1280×720@60.

2) Pipeline GStreamer funcional:
– gst-launch-1.0 -v v4l2src device=/dev/video0 ! image/jpeg,width=1280,height=720,framerate=60/1 ! nvjpegdec ! nvvidconv ! fakesink
– Debes ver negociación correcta y FPS cercanos a 60.

3) Preprocesado CUDA activo:
– Al ejecutar ./cuda_qr_decoder, observa tegrastats: campos como GR3D_FREQ y EMC deberían subir (p. ej., GR3D 30–60%). FPS reportado en [STAT] ≥ 45.

4) Decodificación QR:
– Muestra un QR impreso o en pantalla (tamaño ≥ 3×3 cm a ~30–50 cm de la cámara, buena iluminación). En consola verás [QR] .
– En candump (vcan0 o can0) deben aparecer frames con ID 123 y data correlacionada con el texto (codificada en ASCII).

5) Latencias y métricas:
– En los logs [STAT] verás FPS y dec/s. A 1280×720, GPU activa, espera ~45–60 FPS.
– Con tegrastats, CPU total < 60% y RAM dentro del presupuesto (< 2.5 GB usados por el proceso).

6) SocketCAN:
– ip -details link show can0/vcan0 muestra “UP” y sin demasiados errores/overruns.
– candump imprime tramas con el mismo ritmo de decodificaciones.

7) PyTorch GPU:
– test_torch.py imprime device=cuda, latencias de pocos milisegundos, confirmando que la GPU está disponible para cómputo.

Troubleshooting

1) La cámara no abre (No se pudo abrir la cámara con GStreamer):
– Revisa permisos: ejecuta como usuario con acceso a /dev/video0 o usa sudo.
– Confirma que el pipeline GStreamer es válido: prueba con gst-launch-1.0 antes de OpenCV.
– Cambia MJPEG a YUYV si tu CU30 no expone MJPEG: usa “v4l2src ! video/x-raw,format=YUY2,width=1280,height=720,framerate=60/1 ! videoconvert ! video/x-raw,format=BGR ! appsink”.
– Desconecta otras cámaras que hayan tomado /dev/video0.

2) OpenCV sin módulos CUDA (error en cv::cuda::…):
– Verifica pkg-config –modversion opencv4 y que libopencv-cuda* esté instalado: dpkg -l | grep opencv.
– En JetPack 6, libopencv-dev incluye CUDA por defecto. Si compilaste OpenCV manualmente sin CUDA, reinstala los paquetes de JetPack: sudo apt install –reinstall libopencv-dev.
– Ejecuta nvidia-smi no aplica en Jetson; usa tegrastats para confirmar GPU.

3) FPS bajos (< 30 FPS):
– Asegúrate de modo MAXN y jetson_clocks: sudo nvpmodel -m 0; sudo jetson_clocks.
– Usa entrada MJPEG y nvjpegdec (no decodifiques por CPU).
– Evita –show si no es necesario; la visualización puede consumir CPU.
– Baja a 960×540 o 640×480 para validar si es un tema de resolución.

4) detectAndDecode no detecta QR:
– Aumenta contraste/iluminación; evita reflejos especulares.
– Prueba binarización menos agresiva: cambia kernel de blur a 3×3, o elimina blur.
– Descarga la imagen sin reducir (usa g_bin.download en lugar de resize) para QR pequeños.
– Valida con qrencode/qrtest una matriz de prueba nítida.

5) CAN no aparece (sin can0 tras el overlay):
– dmesg | grep -i mcp2515 para ver si hay errores de IRQ o SPI. Revisa el pin INT y la línea interrupts del overlay (GPIO correcto).
– Verifica el reloj del MCP2515 (8MHz vs 16MHz). Ajusta oscillator-frequency en el overlay según tu módulo.
– Asegura alimentación adecuada del transceptor (muchos requieren 5V).

6) “Cannot find nvjpegdec” en GStreamer:
– Lista plugins: gst-inspect-1.0 | grep -i nvjpegdec. Si falta, reinstala gstreamer1.0-plugins-bad y librerías NVIDIA multimedia: sudo apt install nvidia-jetpack (ya incluye multimedia).
– Alternativa temporal: usa jpegdec (CPU). Penaliza rendimiento: sustituye nvjpegdec por jpegdec.

7) PyTorch no detecta CUDA (torch.cuda.is_available() == False):
– Usa el índice extra de NVIDIA para wheels JetPack 6: pip3 install ‘torch==2.3.0’ –extra-index-url https://developer.download.nvidia.com/compute/redist/jp/v60
– Evita instalar wheels genéricos de PyPI.
– Comprueba librerías del driver: dpkg -l | grep nvidia-cuda.

8) Candump no muestra nada:
– En vcan0, asegúrate de que la app esté apuntando a vcan0 y que candump también.
– En CAN físico, comprueba bitrate (ambos nodos deben tener el mismo), terminadores, polaridad CAN_H/CAN_L, y que haya al menos dos nodos en el bus.

Mejoras/variantes

  • Zero-copy y menos copias CPU-GPU:
  • Usa nvbuf-metadata y memoria NVMM para evitar conversión a BGRx/videoconvert, o integra un appsink que entregue NvBufSurface y convierta a cv::cuda::GpuMat mediante mapeo EGL.
  • Decodificación híbrida:
  • Mantén el preprocesado en GPU y sustituye QRCodeDetector por ZXing-C++ para mayor robustez en QR dañados; integra el lector con “LuminanceSource” sobre la matriz binaria.
  • Multiproceso/cámaras múltiples:
  • Crea hilos por cámara y usa un socket CAN por hilo, o multiplexa en un reactor epoll para controlar bufferbloat.
  • Telemetría:
  • Exporta métricas de FPS, latencias p50/p95, tasa de aciertos, carga GR3D/EMC a Prometheus (node exporter custom) y grafica en Grafana.
  • Pipeline alternativo con DeepStream:
  • Implementa un plugin custom (GstBaseTransform) para decodificar QR en CUDA e integrarlo en un pipeline DeepStream si necesitas RTSP/RTMP out o multipistas con OSD.
  • Compilación a ARMv8.2 con NEON:
  • Añade fallback CPU vectorizado cuando no hay GPU disponible (útil si migras a plataformas sin CUDA).

Checklist de verificación

  • [ ] cat /etc/nv_tegra_release muestra L4T 36.3 y JetPack 6.0.1.
  • [ ] nvcc –version reporta CUDA 12.2.2; pkg-config –modversion opencv4 reporta 4.8.0.
  • [ ] gst-launch-1.0 con nvjpegdec funciona a 1280×720@60.
  • [ ] Compilé correctamente cuda_qr_decoder (cmake + make).
  • [ ] Puedo levantar vcan0 o can0 (SocketCAN) y ver tramas con candump.
  • [ ] La app alcanza ≥ 45 FPS a 1280×720, con GR3D > 30% durante actividad.
  • [ ] Leo y decodifico un QR real; veo [QR] en consola y tramas CAN correspondientes.
  • [ ] test_torch.py confirma torch=2.3.0 y device=cuda con latencias de un dígito de ms.
  • [ ] Revertí nvpmodel/jetson_clocks a valores normales si es necesario.

Anexo: comandos útiles y visuales

  • Verificar Jetson y paquetes NVIDIA:
  • uname -a
  • cat /etc/nv_tegra_release
  • dpkg -l | grep -E ‘nvidia|tensorrt’
  • Listar cámaras y formatos:
  • v4l2-ctl –list-devices
  • v4l2-ctl –device=/dev/video0 –list-formats-ext
  • Ajustar exposición/ganancia (si necesario):
  • v4l2-ctl -d /dev/video0 -c exposure_auto=1 -c exposure_absolute=100
  • Monitoreo:
  • tegrastats
  • top -H -p $(pidof cuda_qr_decoder)

Con esto dispones de un caso práctico completo y reproducible, centrado en el “cuda-qr-decoder” con la plataforma exacta Jetson Orin Nano 8GB Developer Kit + e-con Systems See3CAM_CU30 (AR0330) + MCP2515, alineado con una toolchain concreta de JetPack 6.0.1 y componentes acelerados por GPU.

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 pipeline CUDA mencionado?




Pregunta 2: ¿A qué velocidad debe funcionar la lectura de QR en líneas de producción?




Pregunta 3: ¿Qué tipo de dispositivos se beneficiarán de este sistema?




Pregunta 4: ¿Qué tipo de procesamiento se realiza en la GPU?




Pregunta 5: ¿Cuál es la latencia máxima permitida para la decodificación por frame?




Pregunta 6: ¿Qué tecnología se usa para la publicación de datos?




Pregunta 7: ¿Cuál es la tasa mínima de detección necesaria para los códigos QR?




Pregunta 8: ¿Qué tipo de cámara se menciona en el artículo?




Pregunta 9: ¿Qué sistema operativo se asume para el desarrollo?




Pregunta 10: ¿Qué tipo de kit se menciona en los prerrequisitos?




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

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

Sígueme:
Scroll al inicio