YAML Metadata Warning:empty or missing yaml metadata in repo card

Check out the documentation for more information.

Cacao Detector — Raspberry Pi

La primera instalación la hace un técnico. Después, la Pi arranca la detección al encender y revisa periódicamente las versiones estables del repositorio público eingel/cacao-detector. Sin Internet continúa con la última versión instalada. El usuario final solo abre http://<ip-de-la-pi>:8000.

Primera instalación (técnico)

En Raspberry Pi OS, copia esta carpeta deploy/ a la Pi y ejecuta una vez:

sudo ./install.sh

El instalador crea /opt/cacao-detector/current (versión activa), /var/lib/cacao-detector/config.json (ajustes locales), cacao.service y cacao-update.timer. Necesita Internet en la primera instalación para obtener la versión estable y las dependencias. Por defecto el servo inicia en modo sim; antes de conectar el expulsor real calibra sus tiempos y cambia servo a 18. Este archivo y camara.sh no se reemplazan al actualizar.

Uso como app: con escritorio y Chromium, la Pi abre la app a pantalla completa al encender (kiosko). Todos los ajustes (umbral, fondo, línea de disparo, servo, cámara) se cambian desde el botón ⚙ de la app y se guardan en config.json: umbral, fondo, sentido y línea de disparo se aplican al instante; servo y cámara reinician la app sola. Las opciones de línea de comandos quedan para mantenimiento. El modo de detección es automático: con el clasificador de recortes usa detección por color + tracking (antes --classic).

Actualizaciones con consentimiento: cada 15 min la Pi descarga y prepara la versión estable nueva, pero no la instala. La app muestra "Hay una actualización disponible" y se aplica al tocar Actualizar ahora o al reiniciar la Pi. Si la versión nueva no produce video, vuelve sola a la anterior y la descarta. (cacao-update.timer → --stage; cacao-apply.path → --apply; cacao-apply-boot.service → --apply-boot.) No actives el servo sin una fuente externa de 5 V y tierra común.

Para comprobar instalación y estado:

systemctl status cacao.service cacao-update.timer
curl -f http://127.0.0.1:8000/health
journalctl -u cacao.service -u cacao-update.service -n 100

La actualización consulta stable.json al arrancar y cada 15 minutos. Descarga una revisión completa, comprueba hashes y dependencias, activa la nueva versión y verifica dos fotogramas recientes. Si falla, vuelve a la anterior y descarta esa revisión hasta que se publique una distinta.

Desarrollo y publicación (técnico)

inference_v2.py conserva la CLI existente. La aplicación está separada en cacao/camera.py, vision.py, tracking.py, servo.py, display.py, web.py y app.py. Ejecuta el selftest y las pruebas del actualizador antes de publicar:

python3 inference_v2.py --selftest
python3 -m unittest discover -s tests -v
python3 publish.py --publish

publish.py sube los archivos y modelos al repositorio, obtiene el SHA del commit y publica stable.json al final. El repositorio debe ser público; la Pi no guarda sesión ni token. El publicador sí necesita una sesión de Hugging Face. Si solo haces cambios locales, ningún equipo se actualizará. El temporizador ejecuta bootstrap.py fuera de la versión activa; si el actualizador nuevo no arranca, usa el de la versión anterior.

Calibración de cámara

Detén el servicio durante la calibración para liberar la cámara y ejecuta enfocar.py con CACAO_DATA_DIR=/var/lib/cacao-detector; su botón «Guardar» escribe camara.sh fuera de las versiones. Reinicia el servicio al terminar.

Captura de datos para reentrenar

En una sesión técnica, usa --detect-only --capture /var/lib/cacao-detector/capturas/lote1. La captura continua está desactivada en el servicio para no llenar el disco.

Reentrenar con DATOS REALES (el paso que arregla la clasificación de verdad)

El modelo actual aprendió de datos públicos ("claro=good, oscuro=bad") y bajo la luz real se equivoca (ej: grano cortado = good 1.00). El fix definitivo:

  1. Primero fija el color: python3 enfocar.py → slider de balance de blancos hasta que la banda sea VERDE y el grano MARRÓN → Guardar. (El WB automático se engaña con la banda verde y pone todo rosado.)
  2. Captura en la Pi (con esa luz ya congelada): python3 inference_v2.py --web --detect-only --capture /var/lib/cacao-detector/capturas/lote1 Pasa granos variados; guarda un recorte por grano cada pocos frames.
  3. Tráelos al Mac: scp -r <usuario>@<ip-pi>:/var/lib/cacao-detector/capturas .
  4. Ordénalos a mano (arrastrar a carpetas, ~30 min para unos cientos): datasets/raw/reales/good/ y datasets/raw/reales/bad/
  5. Regenera y reentrena: python3 scripts/build_beans_cls_dataset.py (incluye los reales automáticamente, oversample x3) → subir el nuevo beans_cls_v2.zip a Colab → mismo notebook → subir cls_ncnn_model al repo.

Reentrenar el clasificador binario (Colab)

  1. En el Mac: python3 scripts/build_beans_cls_dataset.py genera datasets/colab/beans_cls_v2.zip (tiles sintéticos tipo banda verde con blending/sombras/rotación).
  2. Subir el zip a Colab y correr notebooks/entrenar_clasificador_binario_v4.ipynb.
  3. Descargar cls_ncnn_model.zip y descomprimirlo en deploy/models/cls_ncnn_model.
  4. Publica una versión estable con python3 publish.py --publish. La Pi descargará el código y el modelo cuando tenga Internet; el servicio se reiniciará tras validar la descarga.

Opciones útiles

Flag Qué hace
--bg green|light|dark Color del fondo/banda (default: green)
--detect-only Solo detecta, no clasifica (rápido; para captura)
--capture DIR Guarda recortes de cada grano en DIR
--conf 0.4 Umbral de confianza del clasificador
--every N (solo con modelo detector) inferir 1 de cada N frames

Expulsor con servo (prototipo v0.2)

Aleta diagonal sobre la banda, aguas abajo de la cámara, que desvía los granos bad hacia el borde/canaleta.

Cómo decide (una sola vez por grano): mientras el grano viaja se muestra neutro (amarillo bean) y va acumulando clasificaciones espaciadas en el tiempo (vistas distintas del grano, no ráfagas del mismo instante). Al cruzar la línea de disparo decide por mayoría de toda la evidencia y ahí queda fijo: good/bad. Solo bad con confianza ≥ --bad-conf dispara el servo; dudoso (bad de baja confianza) pasa como good. Con el clasificador bad/good el default es 0.4 (calibrado contra granos juzgados); el recorte se aplasta a 128×128 con un 15 % de margen, igual que en su entrenamiento. Esto evita el parpadeo good/bad y que el servo actúe sobre una lectura temprana equivocada.

Nota: los contadores good/bad suben al cruzar la línea (no antes).

 [cámara]                →  sentido banda  →
 ┌──────────────┐
 │ ····· | ← línea (--trigger 0.8)
 └──────────────┘
        ├─ d ≥ 15 cm ─┤ [ALETA servo] → canaleta lateral

Montaje: aleta FUERA del encuadre, ≥15-20 cm después de la línea (la decisión tarda ~0.5-1 s; d ≥ velocidad_banda × 1 s). La aleta debe descansar sola en reposo (gravedad o tope mecánico): el código suelta el servo (detach) cuando no expulsa, así que en reposo no hay torque de sostén.

Cableado del servo (MG90S / SG90, 3 cables)

Cable Señal Va a
Amarillo (naranja) PWM GPIO 18 = pin físico 12
Rojo +5V Fuente externa 5V ≥1A (NO el pin 5V de la Pi)
Marrón (café) GND GND de la fuente + GND de la Pi (pin 6) = tierra común

⚠️ El MG90S puede jalar ~1A en el arranque; alimentarlo desde la Pi la reinicia o corrompe la SD. La tierra común (fuente↔Pi) es obligatoria o el servo no recibe la señal.

Servo por software

install.sh instala gpiozero y lgpio; el entorno Python puede usar los paquetes del sistema.

Prueba del servo solo (sin cámara ni modelo):

python3 -c "
from gpiozero import AngularServo
from time import sleep
s = AngularServo(18, min_angle=0, max_angle=180, initial_angle=0)
for _ in range(3):
    s.angle = 80; sleep(0.6)
    s.angle = 0;  sleep(0.6)
"

Debe moverse 3 veces de 0°→80° y volver. El warning PWMSoftwareFallback es normal y no importa: el código suelta el servo en reposo, así que ese jitter no ocurre mientras espera.

Calibración (sin hardware, primero en pantalla):

python3 inference_v2.py --web --servo sim
  1. Verifica el sentido con --flow lr o --flow rl (la línea debe estar cerca de donde los granos SALEN del encuadre).
  2. Pasa un grano malo: al cruzar la línea se pone roja + "EJECT" tras --eject-delay segundos. Cronometra cuánto tarda el grano real de la línea a la posición física de la aleta y ajusta --eject-delay.
  3. Con el servo conectado, cambia sim por el pin:
python3 inference_v2.py --web --servo 18 \
    --eject-delay 0.8 --eject-hold 0.5 --eject-angle 80
Flag Qué calibra
--eject-delay Segundos de viaje línea→aleta (LA perilla principal)
--eject-hold Cuánto queda activa la aleta
--eject-angle / --rest-angle Ángulos activo/reposo del servo
--trigger Posición de la línea (fracción del frame)
--flow lr|rl Sentido de la banda en cámara
--min-area Área mínima (px²) de grano entero: caja menor = pasilla/partido → bad por geometría, sin importar el clasificador. 0 = off
--gate Compuerta 2 vías (demo): la aleta enruta good a un lado y bad al otro en cada grano, en vez de solo actuar con los malos

Modo compuerta 2 vías (--gate)

Para demostración: en vez de "aleta quieta que solo empuja los malos", el servo dirige cada grano — good a un lado, bad al otro — así se ve el sistema decidiendo en vivo. El servo se energiza solo durante el paso del grano y luego se suelta (detach), dejando la aleta en el lado elegido por gravedad/fricción. Esto evita el jitter/convulsión del PWM por software (el MG90S vibra si se lo mantiene energizado permanentemente). Si tu aleta no se sostiene sola en cada lado, hay que pasar a PWM por hardware en GPIO 18.

python3 inference_v2.py --web --servo 18 --gate \
    --rest-angle 30 --eject-angle 120
  • --rest-angle = lado GOOD, --eject-angle = lado BAD. El reposo es el centro entre ambos: cada grano (bueno o malo) hace un swing visible desde el centro a su lado y vuelve. Pon los dos ángulos bien separados (ej. --rest-angle 40 --eject-angle 140) para que el movimiento se note.
  • La línea en pantalla se pinta verde -> GOOD o roja -> BAD según la ruta del último grano.
  • Montaje: aleta pivotante en V al final de la banda, con una canaleta a cada lado. Combínalo con --min-area para que partidos/pasillas vayan al lado BAD.

PWM por hardware (--hw-pwm) — servo firme sin jitter

El PWM por software hace vibrar al MG90S cuando sostiene una posición (no puede mantener el centro quieto en --gate). El GPIO 18 tiene PWM por hardware que da señal perfecta y sostiene firme. Setup (una vez, en la Pi):

En la Pi 4, añade dtoverlay=pwm-2chan a /boot/firmware/config.txt y reinicia. El paquete Python ya está en requirements-runtime.txt. Comprueba que el usuario cacao pueda acceder a /sys/class/pwm; puede requerir una regla de permisos para esa instalación antes de habilitar hw_pwm en config.json.

Luego corre con --hw-pwm:

python3 inference_v2.py --web --servo 18 --gate --hw-pwm \
    --rest-angle 40 --eject-angle 140

Con --hw-pwm el servo sostiene el centro firme entre granos y hace swings limpios a cada lado, sin vibración. (Sin --hw-pwm usa PWM software: se suelta en reposo para no vibrar, pero la aleta se cae por gravedad.)

Calibrar --min-area: cada caja muestra su tamaño WxH en px. Mira un grano entero típico (ej. 90x120 ≈ 10800 px²) y pon el umbral en ~55-60%: --min-area 6000. Mitades, pedazos y pasillas caen debajo y se expulsan aunque el clasificador diga good (una mitad con el corte hacia abajo es invisible para cualquier modelo, pero su tamaño la delata).

Requisito físico: granos SEPARADOS en la banda — dos granos pegados se desvían juntos.

Jitter del servo: en PWM por software el código suelta el servo en reposo; para sostener la posición usa hw_pwm tras configurar el overlay y permisos.

Detener o reiniciar el servicio

sudo systemctl stop cacao.service
sudo systemctl start cacao.service
Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support