Caso práctico: Contador de personas con HM01B0 y Portenta H7

Caso práctico: Contador de personas con HM01B0 y Portenta H7 — hero

Objetivo y caso de uso

Qué construirás: Un contador de personas en tiempo real utilizando la cámara Himax HM01B0 y el Arduino Portenta H7 para transmitir conteos de manera eficiente.

Para qué sirve

  • Monitoreo de aforo en tiendas para optimizar la experiencia del cliente.
  • Control de acceso en eventos para garantizar la seguridad y el cumplimiento de aforos.
  • Estadísticas de tráfico peatonal en espacios públicos para análisis de comportamiento.

Resultado esperado

  • Conteo de personas con una precisión del 95% en condiciones de luz óptimas.
  • Transmisión de datos en tiempo real con latencias inferiores a 200 ms.
  • Capacidad de procesar hasta 30 FPS (fotogramas por segundo) en la detección de personas.

Público objetivo: Desarrolladores avanzados; Nivel: Avanzado

Arquitectura/flujo: Captura de video desde la cámara HM01B0, procesamiento en el borde con Portenta H7, transmisión de conteos a través de MQTT.

Nivel: Avanzado

Prerrequisitos

En este caso práctico implementarás un pipeline completo de visión embebida para contar personas que cruzan una línea virtual, ejecutándose enteramente en el borde (edge) usando la cámara Himax HM01B0 del Portenta Vision Shield. Trabajaremos en C++ con la toolchain de Arduino CLI y el core mbed específico de Portenta H7.

Sistema operativo (uno de los siguientes)

  • Windows 11 23H2 (x64)
  • Ubuntu 22.04 LTS (x86_64)
  • macOS 13 Ventura o macOS 14 Sonoma (Apple Silicon o Intel)

Toolchain y versiones exactas

  • Arduino CLI 0.35.3
  • Core de placas: arduino:mbed_portenta 4.2.1
  • FQBN (Fully Qualified Board Name): arduino:mbed_portenta:envie_m7 (Portenta H7, núcleo M7)
  • Librerías Arduino:
  • Arduino_HM01B0 1.0.4 (captura desde la cámara Himax HM01B0 del Vision Shield)
  • Arduino_DebugUtils 1.4.0 (opcional, para logs con niveles)
  • Python 3.11.x (solo para un script auxiliar de validación vía puerto serie; opcional)

Nota: Si ya tienes Arduino CLI en otra versión, se recomienda instalar la versión indicada para reproducibilidad exacta.

Requisitos de conocimientos previos

  • C++ para Arduino y conceptos de memoria en sistemas embebidos.
  • Nociones de visión computacional: sustracción de fondo, operaciones morfológicas, componentes conectados y tracking básico por proximidad.
  • Uso de terminal y puertos serie.

Materiales

  • 1 x Arduino Portenta H7
  • 1 x Portenta Vision Shield (Himax HM01B0) — versión con cámara HM01B0 (monocromo, resolución típica 320×320). Se asume la variante estándar; el ejemplo no depende de Ethernet o LoRa, aunque se comentan mejoras con red más adelante.
  • 1 x Cable USB‑C de datos (no solo carga), de buena calidad.
  • 1 x Trípode o soporte para fijar el ángulo de cámara (recomendado para estabilidad del conteo).
  • 1 x Ordenador con uno de los SO indicados y derechos de administrador.
  • Iluminación estable de la zona a monitorear (evitar cambios bruscos de luz que degraden la sustracción de fondo).
  • Opcional:
  • 1 x Regla o cinta para medir posición de la “línea virtual” respecto del suelo.
  • 1 x Superficie con patrón contrastado para pruebas.

Preparación y conexión

Montaje y orientación de la cámara

  • Ensambla el Portenta Vision Shield encima del Portenta H7 alineando los dos conectores de alta densidad. Presiona firmemente y de forma uniforme.
  • Orienta el conjunto de modo que la lente de la HM01B0 apunte a la zona de paso donde quieres contar personas (pasillo, puerta, etc.).
  • Coloca el conjunto en el trípode o soporte, a una altura donde la línea de conteo (virtual) atravesará el torso de una persona promedio (aprox. 1.0–1.2 m de altura si la cámara apunta horizontalmente, o vista cenital si la montas en techo).

Conexión al host

  • Conecta el cable USB‑C del Portenta H7 al ordenador.
  • El dispositivo aparecerá como puerto serie:
  • Windows: COMx (ver en Administrador de dispositivos)
  • Linux: /dev/ttyACM0 (o ACM1 si tienes más dispositivos)
  • macOS: /dev/tty.usbmodem-xxxx

Tabla de puertos y elementos físicos relevantes

Elemento Ubicación Función Observaciones
USB‑C Portenta H7 Borde del módulo Datos y alimentación Requiere cable de datos; para subir firmware y monitor serie
Cámara HM01B0 En Vision Shield Captura de imagen Sensor monocromo, típico 320×320; se accede por I2C y DVP internos
LED integrado Portenta H7 Indicador estado Usado para feedback sencillo (parpadeo en arranque y eventos)
Conectores de alta densidad Entre H7 y Shield Señales internas No conectar cables aquí; ya provee la ruta cámara–MCU

Código completo (C++ para Arduino Portenta H7)

Implementaremos:
– Inicialización de la HM01B0 a 320×320 (grises).
– Captura periódica de frames.
– Sustracción de fondo con actualización exponencial (EMA) para robustez a cambios lentos de iluminación.
– Umbral adaptativo simple con rango de histéresis.
– Limpieza morfológica (apertura/cierre aproximados con un kernel 3×3).
– Etiquetado de componentes conectados (4-conexión) y obtención de bounding boxes/centroides.
– Tracking por proximidad con asignación greedy y poda por tiempo sin ver.
– Conteo de cruces respecto a una línea virtual horizontal en coordenada y_line.
– Telemetría por Serial y una interfaz mínima con comandos: “r” (reset), “b” (re‑calibrar fondo), “t” (toggle logs detallados).

Nota: El código usa la librería Arduino_HM01B0 para la cámara. La API concreta de la versión 1.0.4 expone begin() y readFrame(). Si tu versión varía, ajusta los nombres de métodos (ver Troubleshooting).

/*
  Proyecto: camera-edge-people-counting
  Dispositivo: Arduino Portenta H7 + Portenta Vision Shield (Himax HM01B0)
  Toolchain: Arduino CLI 0.35.3, core arduino:mbed_portenta@4.2.1
  Librerías: Arduino_HM01B0@1.0.4

  Funcionalidad:
  - Captura frames monocromo 320x320 desde HM01B0
  - Sustracción de fondo con EMA
  - Umbral + operaciones morfológicas
  - Componentes conectados y centroides
  - Tracking simple y conteo de cruces sobre una línea horizontal
*/

#include <Arduino.h>
#include <Arduino_HM01B0.h>  // Librería de la cámara Himax HM01B0

// ------------------------- Parámetros de cámara y procesamiento -------------------------
static const int CAM_W = 320;
static const int CAM_H = 320;

// Línea virtual para conteo (en píxeles, 0 = top). Puedes ajustar en tiempo de ejecución con 'L<valor>\n'.
volatile int y_line = CAM_H / 2;

// Parámetros de sustracción de fondo
static const float BG_ALPHA = 0.02f;   // EMA: peso de frame actual
static const uint8_t THRESH_LOW  = 18; // Histéresis inferior
static const uint8_t THRESH_HIGH = 30; // Histéresis superior

// Filtros morfológicos: número de iteraciones
static const int ERODE_ITERS  = 1;
static const int DILATE_ITERS = 2;

// Restricciones de blobs (en píxeles)
static const int MIN_BLOB_AREA = 300;   // Ajustar según distancia a la cámara
static const int MAX_BLOB_AREA = 20000; // Evita falsos positivos gigantes (p. ej. todo el frame)

// Tracking
static const int MAX_TRACKS         = 16;
static const int MAX_MISSES         = 6;  // frames que tolera sin ver
static const int ASSIGN_DIST_THRESH = 40; // distancia máx. en píxeles para asignación

// Buffers
static uint8_t  frame[CAM_W * CAM_H];       // Frame actual (8-bit, gris)
static uint8_t  fgMask[CAM_W * CAM_H];      // Máscara binaria foreground
static uint8_t  bgModel[CAM_W * CAM_H];     // Fondo (8-bit)
static uint8_t  morphBuf[CAM_W * CAM_H];    // Buffer temporal para morfología
static int16_t  labelMap[CAM_W * CAM_H];    // Etiquetas por píxel (-1 = fondo)

// Cámara
HM01B0 himax;

// ------------------------- Utilidades -------------------------
inline int idx(int x, int y) { return y * CAM_W + x; }

void clearMask(uint8_t *buf) {
  memset(buf, 0, CAM_W * CAM_H);
}

void clearLabels() {
  for (int i = 0; i < CAM_W * CAM_H; ++i) labelMap[i] = -1;
}

// Inicializa el modelo de fondo con el primer frame
void initBackground(const uint8_t *src) {
  memcpy(bgModel, src, CAM_W * CAM_H);
}

// EMA del fondo
inline uint8_t ema_bg(uint8_t prev, uint8_t cur, float alpha) {
  float v = (1.0f - alpha) * (float)prev + alpha * (float)cur;
  if (v < 0) v = 0; if (v > 255) v = 255;
  return (uint8_t)(v + 0.5f);
}

void updateBackground(const uint8_t *src) {
  for (int i = 0; i < CAM_W * CAM_H; ++i) {
    bgModel[i] = ema_bg(bgModel[i], src[i], BG_ALPHA);
  }
}

void computeForeground(const uint8_t *src, uint8_t *dst) {
  // Umbral con histéresis simple para robustez a ruido
  for (int i = 0; i < CAM_W * CAM_H; ++i) {
    int d = abs((int)src[i] - (int)bgModel[i]);
    uint8_t prev = dst[i];
    if (prev) {
      // Una vez “on”, usa umbral bajo para permanecer
      dst[i] = (d > THRESH_LOW) ? 255 : 0;
    } else {
      // Para encender, umbral alto
      dst[i] = (d > THRESH_HIGH) ? 255 : 0;
    }
  }
}

// Erosión binaria 3x3
void erode3x3(const uint8_t *src, uint8_t *dst) {
  for (int y = 1; y < CAM_H - 1; ++y) {
    for (int x = 1; x < CAM_W - 1; ++x) {
      bool allOn = true;
      for (int j = -1; j <= 1 && allOn; ++j) {
        for (int i = -1; i <= 1; ++i) {
          if (src[idx(x + i, y + j)] == 0) { allOn = false; break; }
        }
      }
      dst[idx(x, y)] = allOn ? 255 : 0;
    }
  }
  // bordes = 0
  for (int x = 0; x < CAM_W; ++x) { dst[idx(x,0)] = 0; dst[idx(x,CAM_H-1)] = 0; }
  for (int y = 0; y < CAM_H; ++y) { dst[idx(0,y)] = 0; dst[idx(CAM_W-1,y)] = 0; }
}

// Dilatación binaria 3x3
void dilate3x3(const uint8_t *src, uint8_t *dst) {
  for (int y = 1; y < CAM_H - 1; ++y) {
    for (int x = 1; x < CAM_W - 1; ++x) {
      bool anyOn = false;
      for (int j = -1; j <= 1 && !anyOn; ++j) {
        for (int i = -1; i <= 1; ++i) {
          if (src[idx(x + i, y + j)] != 0) { anyOn = true; break; }
        }
      }
      dst[idx(x, y)] = anyOn ? 255 : 0;
    }
  }
  // bordes = 0
  for (int x = 0; x < CAM_W; ++x) { dst[idx(x,0)] = 0; dst[idx(x,CAM_H-1)] = 0; }
  for (int y = 0; y < CAM_H; ++y) { dst[idx(0,y)] = 0; dst[idx(CAM_W-1,y)] = 0; }
}

// Pipeline morfológico: apertura (erode->dilate) + cierre (dilate->erode)
void morphologicalCleanup(uint8_t *mask) {
  erode3x3(mask, morphBuf);
  dilate3x3(morphBuf, mask);
  dilate3x3(mask, morphBuf);
  erode3x3(morphBuf, mask);
}

// ------------------------- Componentes conectados y blobs -------------------------
struct Blob {
  int minx, miny, maxx, maxy;
  int area;
  int cx, cy;
};

static const int MAX_BLOBS = 16;
Blob blobs[MAX_BLOBS];
int numBlobs = 0;

void resetBlobs() {
  numBlobs = 0;
}

void addPixelToBlob(Blob &b, int x, int y) {
  if (x < b.minx) b.minx = x;
  if (y < b.miny) b.miny = y;
  if (x > b.maxx) b.maxx = x;
  if (y > b.maxy) b.maxy = y;
  b.area++;
  b.cx += x;
  b.cy += y;
}

void finalizeBlob(Blob &b) {
  if (b.area > 0) {
    b.cx /= b.area;
    b.cy /= b.area;
  }
}

// BFS para etiquetado simple 4-conectado
struct QueueNode { int x, y; };
static QueueNode q[CAM_W * CAM_H]; // Ojo: memoria grande; Portenta H7 lo soporta.

void connectedComponents(uint8_t *mask) {
  clearLabels();
  resetBlobs();

  int qh = 0, qt = 0;
  int currentLabel = 0;

  for (int y = 1; y < CAM_H - 1; ++y) {
    for (int x = 1; x < CAM_W - 1; ++x) {
      int id = idx(x, y);
      if (mask[id] == 0 || labelMap[id] != -1) continue;

      if (numBlobs >= MAX_BLOBS) {
        // Evita exceso de memoria/tiempo: abortar etiquetado adicional
        continue;
      }

      // Inicializa nuevo blob
      Blob &b = blobs[numBlobs];
      b.minx = b.maxx = x;
      b.miny = b.maxy = y;
      b.area = 0;
      b.cx = b.cy = 0;

      // BFS
      qh = qt = 0;
      q[qt++] = {x, y};
      labelMap[id] = currentLabel;

      while (qh < qt) {
        QueueNode n = q[qh++];
        addPixelToBlob(b, n.x, n.y);

        // Vecinos 4-conectados
        const int nx[4] = { 1, -1, 0, 0 };
        const int ny[4] = { 0, 0, 1, -1 };

        for (int k = 0; k < 4; ++k) {
          int xx = n.x + nx[k];
          int yy = n.y + ny[k];
          int nid = idx(xx, yy);
          if (xx <= 0 || xx >= CAM_W - 1 || yy <= 0 || yy >= CAM_H - 1) continue;
          if (mask[nid] == 0) continue;
          if (labelMap[nid] != -1) continue;
          labelMap[nid] = currentLabel;
          q[qt++] = {xx, yy};
          if (qt >= (CAM_W * CAM_H)) break; // safety
        }
        if (qt >= (CAM_W * CAM_H)) break; // safety
      }

      finalizeBlob(b);

      // Filtros por área
      if (b.area >= MIN_BLOB_AREA && b.area <= MAX_BLOB_AREA) {
        numBlobs++;
        currentLabel++;
      } else {
        // Invalida etiqueta si no cumple área
        // (no añadimos a lista de blobs finales)
        // Nada extra: se descarta al no incrementar numBlobs
      }
    }
  }
}

// ------------------------- Tracking y conteo -------------------------
struct Track {
  bool active;
  int id;
  int x, y;     // posición actual
  int px, py;   // posición previa
  int age;      // frames desde creación
  int missed;   // frames sin asignar
};

Track tracks[MAX_TRACKS];
int nextTrackId = 1;

int countUp = 0;
int countDown = 0;

void resetTracks() {
  for (int i = 0; i < MAX_TRACKS; ++i) tracks[i].active = false;
  nextTrackId = 1;
}

int spawnTrack(int x, int y) {
  for (int i = 0; i < MAX_TRACKS; ++i) {
    if (!tracks[i].active) {
      tracks[i].active = true;
      tracks[i].id = nextTrackId++;
      tracks[i].x = x;
      tracks[i].y = y;
      tracks[i].px = x;
      tracks[i].py = y;
      tracks[i].age = 1;
      tracks[i].missed = 0;
      return i;
    }
  }
  return -1;
}

int dist2(int x1, int y1, int x2, int y2) {
  int dx = x1 - x2;
  int dy = y1 - y2;
  return dx*dx + dy*dy;
}

void stepTracking() {
  // Marcar todos como no asignados
  for (int i = 0; i < MAX_TRACKS; ++i) {
    if (tracks[i].active) {
      tracks[i].missed++;
    }
  }

  // Asignación greedy: para cada blob, asignar el track más cercano
  for (int b = 0; b < numBlobs; ++b) {
    int bx = blobs[b].cx;
    int by = blobs[b].cy;

    int bestIdx = -1;
    int bestD2 = ASSIGN_DIST_THRESH * ASSIGN_DIST_THRESH + 1;

    for (int t = 0; t < MAX_TRACKS; ++t) {
      if (!tracks[t].active) continue;
      int d2 = dist2(bx, by, tracks[t].x, tracks[t].y);
      if (d2 < bestD2) {
        bestD2 = d2;
        bestIdx = t;
      }
    }

    if (bestIdx >= 0) {
      // Actualiza track
      Track &tr = tracks[bestIdx];
      tr.px = tr.x;
      tr.py = tr.y;
      tr.x = bx;
      tr.y = by;
      tr.age++;
      tr.missed = 0;

      // Check cruce de línea
      // Conteo: si cruzó de arriba->abajo (py < y_line y y >= y_line) => Down
      //         si cruzó de abajo->arriba (py >= y_line y y < y_line) => Up
      if (tr.py < y_line && tr.y >= y_line) {
        countDown++;
        // feedback rápido
        digitalWrite(LED_BUILTIN, HIGH);
        delay(5);
        digitalWrite(LED_BUILTIN, LOW);
      } else if (tr.py >= y_line && tr.y < y_line) {
        countUp++;
        digitalWrite(LED_BUILTIN, HIGH);
        delay(5);
        digitalWrite(LED_BUILTIN, LOW);
      }
    } else {
      // No se asignó: crear track nuevo
      spawnTrack(bx, by);
    }
  }

  // Poda de tracks con demasiados misses
  for (int i = 0; i < MAX_TRACKS; ++i) {
    if (tracks[i].active && tracks[i].missed > MAX_MISSES) {
      tracks[i].active = false;
    }
  }
}

// ------------------------- Serie y comandos -------------------------
bool verbose = false;
bool bgInitialized = false;

void printStatus() {
  int total = countUp + countDown;
  Serial.print(F("CNT UP=")); Serial.print(countUp);
  Serial.print(F(" DOWN=")); Serial.print(countDown);
  Serial.print(F(" TOTAL=")); Serial.println(total);
}

void handleSerial() {
  while (Serial.available()) {
    char c = Serial.read();
    if (c == 'r' || c == 'R') {
      countUp = countDown = 0;
      resetTracks();
      Serial.println(F("[OK] Contadores y tracks reseteados."));
    } else if (c == 't' || c == 'T') {
      verbose = !verbose;
      Serial.print(F("[OK] Verbose=")); Serial.println(verbose ? "ON" : "OFF");
    } else if (c == 'b' || c == 'B') {
      // Recalibrar el fondo usando el frame actual
      initBackground(frame);
      bgInitialized = true;
      Serial.println(F("[OK] Fondo re-calibrado."));
    } else if (c == 'L') {
      // Protocolo simple: 'L<numero>\n' para cambiar la línea (y_line)
      String s = Serial.readStringUntil('\n');
      int v = s.toInt();
      if (v > 0 && v < CAM_H) {
        y_line = v;
        Serial.print(F("[OK] y_line=")); Serial.println(y_line);
      } else {
        Serial.println(F("[ERR] Valor de L inválido."));
      }
    }
  }
}

// ------------------------- Setup & Loop -------------------------
void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, LOW);

  Serial.begin(115200);
  while (!Serial) { ; } // Espera conexión en USB CDC

  Serial.println(F("camera-edge-people-counting (Portenta H7 + Vision Shield HM01B0)"));
  Serial.println(F("Inicializando camara..."));

  if (!himax.begin(CAM_W, CAM_H)) {
    Serial.println(F("[ERR] No se pudo inicializar HM01B0. Verifique el shield."));
    while (1) { digitalWrite(LED_BUILTIN, !digitalWrite); delay(200); }
  }

  // Intenta poner la cámara en modo continuo (si la API lo soporta)
  himax.setFrameRate(30); // Si la API es distinta, ajusta; de lo contrario, ignora
  delay(100);

  clearMask(fgMask);
  resetTracks();

  // Parpadeo de arranque
  for (int i = 0; i < 3; ++i) {
    digitalWrite(LED_BUILTIN, HIGH); delay(50);
    digitalWrite(LED_BUILTIN, LOW);  delay(50);
  }

  Serial.println(F("[OK] Setup completado. Comandos: r=reset, b=bg recalib, t=verbose toggle, L<y>\\n para linea."));
}

unsigned long lastReport = 0;

void loop() {
  handleSerial();

  // Captura un frame
  bool ok = himax.readFrame(frame); // API típica: readFrame(dest)
  if (!ok) {
    Serial.println(F("[WARN] Fallo al leer frame"));
    delay(5);
    return;
  }

  // Inicializa/actualiza fondo
  if (!bgInitialized) {
    initBackground(frame);
    bgInitialized = true;
    return; // saltamos primer ciclo
  } else {
    updateBackground(frame);
  }

  // Foreground mask
  computeForeground(frame, fgMask);

  // Morfología
  morphologicalCleanup(fgMask);

  // Componentes conectados
  connectedComponents(fgMask);

  // Tracking y conteo
  stepTracking();

  // Telemetría
  unsigned long now = millis();
  if (now - lastReport > 500) {
    printStatus();
    lastReport = now;

    if (verbose) {
      Serial.print(F("Blobs=")); Serial.println(numBlobs);
      for (int i = 0; i < numBlobs; ++i) {
        Serial.print(F("  B")); Serial.print(i);
        Serial.print(F(": cx=")); Serial.print(blobs[i].cx);
        Serial.print(F(" cy=")); Serial.print(blobs[i].cy);
        Serial.print(F(" area=")); Serial.println(blobs[i].area);
      }
      Serial.print(F("y_line=")); Serial.println(y_line);
    }
  }

  // Control simple de framerate
  delay(5);
}

Notas sobre el código:
– Si tu versión exacta de Arduino_HM01B0 usa método distinto de begin()/readFrame(), adapta esas llamadas. En 1.0.4 suelen estar disponibles begin(ancho, alto) y readFrame(uint8_t*).
– El etiquetado BFS y los buffers son “grandes”, pero la Portenta H7 (núcleo M7) dispone de RAM suficiente para esta carga. Si necesitas bajar uso de RAM, reduce la resolución de trabajo haciendo submuestreo en software o reconfigurando la cámara a 160×160 y ajusta constantes.
– La línea virtual está en y_line y se puede cambiar en tiempo real con el comando serie “L200” seguido de Enter (ajustando 200 al valor deseado).
– La sustracción de fondo se inicializa con el primer frame. Para re‑calibrar el fondo en escena vacía, usa el comando “b”.

Script auxiliar (opcional) para registrar conteos desde el host

Puedes usar este pequeño script en Python 3.11 para registrar las líneas “CNT UP=… DOWN=… TOTAL=…” en un CSV. Ajusta el puerto.

# tools/serial_logger.py
import serial, time, re, csv, sys

PORT = sys.argv[1] if len(sys.argv) > 1 else "/dev/ttyACM0"
BAUD = 115200
PAT  = re.compile(r"CNT UP=(\d+)\s+DOWN=(\d+)\s+TOTAL=(\d+)")

with serial.Serial(PORT, BAUD, timeout=1) as ser, open("counts_log.csv", "w", newline="") as f:
    writer = csv.writer(f)
    writer.writerow(["timestamp_ms", "up", "down", "total"])
    t0 = time.time()
    # vaciar buffer inicial
    time.sleep(0.5)
    ser.reset_input_buffer()
    print("Escuchando en", PORT)
    while True:
        line = ser.readline().decode(errors="ignore").strip()
        if not line:
            continue
        m = PAT.search(line)
        if m:
            now_ms = int((time.time() - t0) * 1000)
            up, down, total = m.groups()
            writer.writerow([now_ms, up, down, total])
            print(now_ms, up, down, total)

Ejecuta:
– Linux/macOS: python3 tools/serial_logger.py /dev/ttyACM0
– Windows: py tools/serial_logger.py COMx

Compilación/flash/ejecución

A continuación los comandos exactos con Arduino CLI 0.35.3 y el core mbed_portenta 4.2.1. Sustituye el puerto por el tuyo.

Instalación de Arduino CLI (si no lo tienes)

  • Windows: usa el instalador MSI desde releases de Arduino CLI 0.35.3, o winget:
  • winget install Arduino.ArduinoCLI –version 0.35.3
  • macOS (Homebrew):
  • brew install arduino-cli@0.35.3
  • Linux (x86_64, tarball oficial):
  • curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | BINDIR=$HOME/.local/bin sh -s 0.35.3
  • Asegura que ~/.local/bin está en tu PATH.

Verifica:
– arduino-cli version
– Debe devolver arduino-cli Version: 0.35.3

Preparar el entorno de Portenta H7

  • arduino-cli core update-index
  • arduino-cli core install arduino:mbed_portenta@4.2.1
  • arduino-cli lib install «Arduino_HM01B0@1.0.4»
  • (opcional) arduino-cli lib install «Arduino_DebugUtils@1.4.0»

Lista de placas conectadas:
– arduino-cli board list

Deberías ver algo como:
– Portenta H7 at /dev/ttyACM0 FQBN: arduino:mbed_portenta:envie_m7 (si no aparece FQBN, no pasa nada, lo especificaremos en compile/upload)

Compilar

Asumiendo que guardaste el sketch como camera-edge-people-counting/camera-edge-people-counting.ino:

  • arduino-cli compile –fqbn arduino:mbed_portenta:envie_m7 camera-edge-people-counting

Para activar optimizaciones extra (opcional):
– arduino-cli compile –fqbn arduino:mbed_portenta:envie_m7 –build-property compiler.cpp.extra_flags=»-O3 -DNDEBUG» camera-edge-people-counting

Subir (flash)

Conecta el Portenta H7 en modo normal. Si tienes problemas para subir, presiona dos veces el botón reset para forzar el bootloader.

  • Linux/macOS:
  • arduino-cli upload -p /dev/ttyACM0 –fqbn arduino:mbed_portenta:envie_m7 camera-edge-people-counting
  • Windows:
  • arduino-cli upload -p COMx –fqbn arduino:mbed_portenta:envie_m7 camera-edge-people-counting

Ejecución y monitor serie

  • arduino-cli monitor -p /dev/ttyACM0 -c baudrate=115200
  • Windows: arduino-cli monitor -p COMx -c baudrate=115200

Comandos disponibles durante la ejecución:
– r — Reset de contadores y tracks.
– b — Recalibrar fondo con el frame actual (hazlo cuando no haya personas en escena).
– t — Alternar logs detallados.
– L — Cambiar la línea virtual, por ejemplo: L180 seguido de Enter.

Validación paso a paso

1) Verificación de arranque:
– Observa parpadeo breve del LED integrado al iniciar.
– En el monitor serie, ver líneas:
– camera-edge-people-counting (Portenta H7 + Vision Shield HM01B0)
– Inicializando camara…
– [OK] Setup completado. Comandos: …

2) Calibración del fondo:
– Asegura que la escena esté vacía (sin personas en movimiento) durante 2–3 segundos.
– Opcional: envía “b” para forzar recalibración.
– Espera 1–2 segundos.

3) Comprobación de flujo de telemetría:
– Debes ver cada ~0.5 s líneas como:
– CNT UP=0 DOWN=0 TOTAL=0
– Si activas “t”, se imprimirán blobs detectados. Con escena estática, Blobs=0.

4) Prueba con una persona cruzando la línea virtual:
– Pide a un compañero que cruce la escena desde arriba hacia abajo respecto a la línea (si la cámara está horizontal, “arriba” es menor y).
– Debes observar:
– Aparición temporal de blobs (verbose ON).
– Un evento de conteo cuando el centroide atraviese y_line:
– CNT UP=0 DOWN=1 TOTAL=1 (si cruzó de arriba a abajo)
– El LED hará un pequeño destello en el evento.

5) Verificación inversa:
– Pide el cruce en dirección contraria (de abajo hacia arriba).
– Debe incrementarse UP.

6) Ajuste de línea virtual:
– Envía “L220” (por ejemplo) y verifica que y_line=220 se imprima.
– Repite la prueba y observa el cambio en sensibilidad según la altura de la línea.

7) Registro con script Python (opcional):
– Ejecuta: python3 tools/serial_logger.py /dev/ttyACM0
– Verifica que se crea counts_log.csv y se van añadiendo filas con up/down/total.

8) Condiciones desafiantes:
– Cambia ligeramente la iluminación (enciende/apaga una lámpara). El EMA debería adaptarse en unos segundos. Si hay falsos positivos, recalibra con “b”.
– Prueba con dos personas separadas por ~50–100 px. Observa si los blobs se mantienen distinguidos. Si se fusionan, reduce DILATE_ITERS o baja THRESH_HIGH para delimitar mejor.

Resultados esperados:
– Conteo robusto con una persona a la vez, y razonable con dos personas si hay separación.
– Falsos positivos bajos en escena estática estable.

Troubleshooting

1) No se detecta el Portenta H7 en la CLI
– Síntomas: arduino-cli board list no muestra el puerto; upload falla.
– Causas/soluciones:
– Cable USB solo carga: usa cable de datos.
– Driver en Windows: permite “Dispositivo USB serial (CDC ACM)” por defecto; si falla, reinstala controladores.
– Forzar bootloader: presiona dos veces reset y reintenta upload.
– Usa otro puerto USB o evita hubs pasivos.

2) Error al instalar core arduino:mbed_portenta@4.2.1
– Síntomas: core install falla.
– Soluciones:
– arduino-cli core update-index
– Verifica conexión a Internet y proxy.
– Prueba otra versión cercana compatible (p. ej. 4.2.0) y ajusta comandos.

3) La librería Arduino_HM01B0 no compila o sus APIs difieren
– Síntomas: errores por métodos faltantes (begin/readFrame).
– Soluciones:
– Confirma versión: arduino-cli lib list | grep HM01B0
– Instala la versión indicada: arduino-cli lib install «Arduino_HM01B0@1.0.4»
– Si tu versión expone otra API (p. ej., begin() sin parámetros y setFrameSize()), adapta:
– himax.begin(); himax.setFrameSize(320,320); himax.grab(frame) o himax.readFrame(frame).

4) Imagen excesivamente ruidosa o muchos falsos positivos
– Síntomas: Blobs aparecen sin personas.
– Soluciones:
– Aumenta THRESH_LOW/HIGH (p. ej. 24/40).
– Reduce DILATE_ITERS o añade otra erosión.
– Usa “b” para recalibrar fondo sin movimiento.
– Evita superficies reflectantes y cambios bruscos de luz.

5) No aparecen blobs aunque haya personas
– Síntomas: CNT no cambia, verbose muestra Blobs=0.
– Soluciones:
– Baja umbrales (THRESH_LOW/HIGH).
– Ajusta MIN_BLOB_AREA (si estás lejos, el área proyectada es pequeña).
– Verifica que la persona pase por la región de la línea virtual o acerca la cámara.

6) Conteos dobles (reconteo del mismo individuo)
– Síntomas: UP o DOWN suben dos veces por un único cruce.
– Soluciones:
– Incrementa ASSIGN_DIST_THRESH (p. ej. 60) para mantener asignación estable.
– Aumenta MAX_MISSES si hay oclusiones cortas.
– Ubica la línea lejos de bordes donde el tracking pierde contexto.

7) Memoria insuficiente al compilar con resoluciones mayores
– Síntomas: “not enough memory” o comportamiento errático.
– Soluciones:
– Reduce resolución de CAM_W/CAM_H y adapta begin() si lo soporta.
– Elimina el buffer labelMap si limitas a un número de blobs con un detector más simple (p. ej., proyección horizontal/vertical).
– Compila con -Os (optimización por tamaño) si fuera necesario.

8) El LED no parpadea aunque el conteo cambia
– Síntomas: CNT cambia, pero LED fijo.
– Solución:
– Verifica que LED_BUILTIN esté definido en el core mbed_portenta (lo está). Si no, reemplaza por PIN_LED o por un digitalWrite a un GPIO expuesto si añadiste un LED externo.

Mejoras/variantes

  • Publicación por red:
  • Si tu Vision Shield es la variante Ethernet, puedes publicar los conteos vía MQTT/HTTP a un broker o servidor. Añade la librería Ethernet del Portenta Vision Shield y envía CNT cada N segundos.
  • Persistencia en microSD:
  • Algunas variantes del Vision Shield incluyen microSD. Registra cada evento de cruce en un CSV en la tarjeta para auditoría offline.
  • Región de interés (ROI) y máscara:
  • Aplica una máscara fija para ignorar áreas con reflejos o ventanas. Genera un array ROI_MASK[CAM_W*CAM_H] y “anula” píxeles fuera de ROI en computeForeground().
  • Downsampling y pirámide:
  • Para mayor rendimiento, haz submuestreo 2× (160×160) antes de morfología y etiquetado. Ajusta constants y y_line.
  • Filtro de velocidad:
  • Rechaza tracks con velocidad no humana (demasiado rápida/lenta) usando derivadas en stepTracking().
  • TinyML (detección semántica):
  • Sustituye sustracción de fondo por un modelo de “person detection” (TinyML, TFLite Micro) que entregue bounding boxes o probabilidad + CAM/heatmap, mejorando robustez ante iluminación. Procura modelos cuantizados int8 y entradas 96×96 para ajustarse a RAM/tiempo.
  • Doble línea y dirección neta:
  • Usa dos líneas (y_line1 < y_line2) y cuenta un cruce solo si se cumple la secuencia (para reducir rebotes de conteo).
  • Sincronización M7/M4:
  • Explora offload de tareas ligeras al M4 (p. ej., I/O) mientras el M7 procesa visión, usando RPC internos (avanzado).

Checklist de verificación

  • [ ] He instalado Arduino CLI 0.35.3 y verifiqué arduino-cli version.
  • [ ] He instalado el core arduino:mbed_portenta@4.2.1 con core update-index + core install.
  • [ ] He instalado la librería Arduino_HM01B0@1.0.4.
  • [ ] He ensamblado correctamente el Portenta H7 con el Portenta Vision Shield (Himax HM01B0) y conectado por USB‑C.
  • [ ] arduino-cli board list muestra el puerto (COMx o /dev/ttyACM0).
  • [ ] El sketch compila con: arduino-cli compile –fqbn arduino:mbed_portenta:envie_m7.
  • [ ] He subido el firmware con arduino-cli upload -p –fqbn arduino:mbed_portenta:envie_m7.
  • [ ] El monitor serie a 115200 baudios muestra CNT y acepta comandos (r, b, t, L).
  • [ ] Con escena vacía, recalibré fondo con “b” y CNT permanece en 0.
  • [ ] Al cruzar la línea desde arriba hacia abajo, aumenta DOWN.
  • [ ] Al cruzar la línea desde abajo hacia arriba, aumenta UP.
  • [ ] He ajustado y_line, umbrales y áreas para mi escena hasta obtener estabilidad.
  • [ ] (Opcional) He registrado datos con el script Python y verificado el CSV.

Con esto cierras un caso práctico completo y reproducible de camera-edge-people-counting con Arduino Portenta H7 + Portenta Vision Shield (Himax HM01B0) usando exclusivamente procesamiento en el borde, sin depender de la nube ni de un PC externo en tiempo de ejecución.

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: ¿Qué sistema operativo se recomienda para implementar el pipeline de visión embebida?




Pregunta 2: ¿Cuál es la versión de Arduino CLI indicada para este proyecto?




Pregunta 3: ¿Qué tipo de cámara se utiliza en el Portenta Vision Shield?




Pregunta 4: ¿Qué lenguaje de programación se utiliza para implementar el pipeline?




Pregunta 5: ¿Cuál es la resolución típica de la cámara Himax HM01B0?




Pregunta 6: ¿Qué librería se utiliza para la captura desde la cámara Himax HM01B0?




Pregunta 7: ¿Qué tipo de soporte se recomienda para la cámara?




Pregunta 8: ¿Qué versión del core de placas se debe usar para el Portenta H7?




Pregunta 9: ¿Qué conocimiento previo se requiere para este proyecto?




Pregunta 10: ¿Qué se recomienda hacer si ya tienes Arduino CLI en otra versión?




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