YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
- Cacao Detector — Raspberry Pi
- Primera instalación (técnico)
- Desarrollo y publicación (técnico)
- Calibración de cámara
- Captura de datos para reentrenar
- Reentrenar con DATOS REALES (el paso que arregla la clasificación de verdad)
- Reentrenar el clasificador binario (Colab)
- Opciones útiles
- Expulsor con servo (prototipo v0.2)
- Detener o reiniciar el servicio
- Primera instalación (técnico)
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:
- 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.) - Captura en la Pi (con esa luz ya congelada):
python3 inference_v2.py --web --detect-only --capture /var/lib/cacao-detector/capturas/lote1Pasa granos variados; guarda un recorte por grano cada pocos frames. - Tráelos al Mac:
scp -r <usuario>@<ip-pi>:/var/lib/cacao-detector/capturas . - Ordénalos a mano (arrastrar a carpetas, ~30 min para unos cientos):
datasets/raw/reales/good/ydatasets/raw/reales/bad/ - Regenera y reentrena:
python3 scripts/build_beans_cls_dataset.py(incluye los reales automáticamente, oversample x3) → subir el nuevobeans_cls_v2.zipa Colab → mismo notebook → subircls_ncnn_modelal repo.
Reentrenar el clasificador binario (Colab)
- En el Mac:
python3 scripts/build_beans_cls_dataset.pygeneradatasets/colab/beans_cls_v2.zip(tiles sintéticos tipo banda verde con blending/sombras/rotación). - Subir el zip a Colab y correr
notebooks/entrenar_clasificador_binario_v4.ipynb. - Descargar
cls_ncnn_model.zipy descomprimirlo endeploy/models/cls_ncnn_model. - 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
- Verifica el sentido con
--flow lro--flow rl(la línea debe estar cerca de donde los granos SALEN del encuadre). - Pasa un grano malo: al cruzar la línea se pone roja + "EJECT" tras
--eject-delaysegundos. Cronometra cuánto tarda el grano real de la línea a la posición física de la aleta y ajusta--eject-delay. - Con el servo conectado, cambia
simpor 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
-> GOODo roja-> BADsegú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-areapara 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