ML-pijplijn voor real-time optische sortering: Automatisch systeem voor tomatenclassificatie op hogesnelheidstransportbanden
Samenvatting
De software verwerkt in real-time de spectrale profielen van vruchten (standaardtomaten en kerstomaatjes) terwijl deze met hoge frequentie over transportbanden bewegen. Het systeem classificeert de rijpheidsfase en de productgrootte om vervolgens een pneumatisch uitstootcommando te verzenden.
Het systeem is ontworpen rond een operationele beperking van de productielijn: elke verwerkte batch is homogeen — óf uitsluitend standaardtomaten, óf uitsluitend kerstomaatjes. Het type batch is vooraf bekend en wordt ingesteld door de operator of het bovenliggende systeem voordat er een enkele vrucht onder de sensor passeert. Deze beperking bepaalt de gehele architectuur: twee specifieke classificatiemodellen die worden geselecteerd via een expliciete configuratieparameter, inclusief een fysieke consistentiecontrole om instelfouten te voorkomen.
Systeemarchitectuur en Workflow
De workflow verloopt als volgt:
- Line-Scan Optische Sensor: Genereert ruwe CSV-data / encoderstappen.
- Gegevensverwerking & O(1) Streaming Math (
src/dataloader.py): Verwerkt de ruwe data naar een gestructureerde featuretabel, inclusief de aanduidingischerryper batch. - Training PER BATCH (expliciete batchtype) (
src/trainmodel.py):
- Model STANDAARD: Classificatie in 5 klassen.
- Model CHERRY: Classificatie in 3 klassen.
- Embedded C++ Firmware Vertaling (
src/export_embedded.py): Vertaalt beide modellen naar één headerbestand. - include/tomato_classifier.h: Bevat de uiteindelijke logica voor uitvoering op STM32 / PLC / ESP32:
enum TomatoBatchMode { BATCHSTANDARD, BATCHCHERRY }tomatocheckbatchanomaly(mode, transitlen)predicttomatoclass(mode, input)
TomatoBatchMode is een lijnconfiguratie die één keer per shift of batch wordt ingesteld door de integrator — vergelijkbaar met het instellen van een "programma" op industriële machines — en is geen kenmerk dat per vrucht door de sensor wordt berekend.
Structuur van de Repository
TOMATO-GRADING-ML/
│
├── .github/
│ └── workflows/
│ └── tests.yml # CI: voert tests/ uit bij elke push/PR
│
├── .gitignore
├── LICENSE
├── requirements.txt
├── requirements-dev.txt # Bevat pytest, uitsluitend voor ontwikkeling/testen
├── README.md # Dit document
│
├── data/
│ ├── raw/ # Ruwe CSV-datasets (veldcampagnes)
│ ├── processed/ # Schone tabeldataset (tomatoes_features.csv)
│ ├── schema/ # Data Dictionary (optical_sensor_data_dictionary.xlsx)
│ └── photos/ # Visuele referenties en monsters voor classificatie
│
├── docs/
│ ├── sorting_classes_taxonomy.md # Taxonomie van de 8 klassen en pneumatische uitstootlogica
│ └── images/ # Grafieken (confusion matrix, feature importance)
│
├── firmware/
│ └── tomato_esp32_test/ # Benchmark-sketch op ESP32 (reële hardwarelatentie)
│ ├── tomato_esp32_test.cpp
│ ├── tomato_core.c
│ └── tomato_classifier.h
│
├── include/
│ └── tomato_classifier.h # Gegenereerde C/C++ firmware (2 modellen, expliciete batch-modus)
│
├── models/
│ ├── model_standard_metadata.json # Model-metadata (trainingsdatum, aantal monsters)
│ └── model_cherry_metadata.json
│
├── src/
│ ├── data_loader.py # Opschonen, hardwarefiltering en sessiebeheer
│ ├── train_model.py # Training per batch, GroupKFold validatie, model-persistentie
│ └── export_embedded.py # Vertaler van model naar embedded C-code
│
└── tests/
├── conftest.py
├── test_data_loader.py
├── test_train_model.py
└── test_export_embedded.py # Bevat een test die de C-header compileert met gcc
Gegevensverwerking en Streaming Math
De ruwe data is afkomstig van sensoren die het passeren van vruchten scannen in dwarsdoorsneden langs de longitudinale as. De module src/data_loader.py implementeert het volgende:
- Hardware Gatekeeper Filtering: Verwijdert ruis van de transportband via het optische validatieregister (
validity_flag == 0→ geldig). - Fruit Sessionization: Herkent de stijgende flank van de scanteller (
frame_id == 1) om lineaire metingen te groeperen in één feature-vector per fysieke vrucht. - Streaming Spectrometry (O(1)): On-the-fly berekening van gemiddelden, standaarddeviaties en gecombineerde chemische indices (ratio's tussen kanalen).
De output is data/processed/tomatoesfeatures.csv, met één rij per vrucht en een kolom ischerry die de batch van herkomst identificeert. Dit wordt gebruikt om elke vrucht tijdens de training naar het juiste model te sturen.
Taxonomie van Klassen
| ID | Klasse | Batch Type | Resultaat |
|---|---|---|---|
| 0 | Groene standaardtomaat | STANDARD | Reject (Afkeur) |
| 1 | Geel-groene tomaat | STANDARD | Reject (Afkeur) |
| 2 | Oranje-gele tomaat | STANDARD | Pass (Acceptatie) |
| 3 | Rood-oranje tomaat | STANDARD | Pass (Acceptatie) |
| 4 | Rode standaardtomaat | STANDARD | Pass (Acceptatie) |
| 5 | Gele kerstomaat | CHERRY | Pass (Acceptatie) |
| 6 | Rode kerstomaat | CHERRY | Pass (Acceptatie) |
| 7 | Donkergroene gevlekte kerstomaat | CHERRY | Reject (Afkeur) |
Volledige details over de taxonomie en de regel voor grootte-identificatie (gebaseerd op transitlen) zijn te vinden in docs/sortingclasses_taxonomy.md.
Architectuur met twee modellen (Batch Mode)
Omdat batches in het veld altijd homogeen zijn en het type vooraf bekend is, behandelt het systeem is_cherry niet als een meting die per vrucht moet worden afgeleid. In plaats daarvan wordt het behandeld als een expliciete configuratieparameter die bepaalt welk van de twee dedicated modellen wordt gebruikt.
# src/train_model.py
BATCH_CONFIG = {
"standard": {"is_cherry_value": 0, "classes": [0, 1, 2, 3, 4]},
"cherry": {"is_cherry_value": 1, "classes": [5, 6, 7]},
}
De functie trainandevaluatemodel(datadir, batch_type="standard") filtert de dataset op batch-type vóór de training en produceert een specifiek model met 5 of 3 klassen. Deze scheiding biedt twee concrete voordelen:
- Elk model lost een eenvoudiger probleem op (5 of 3 klassen in plaats van 8), waardoor het niet opnieuw het onderscheid tussen standaard en cherry hoeft te leren bij elke voorspelling.
- De firmware-interface is expliciet: de integrator van
tomatoclassifier.hdeclareert één keer per shiftBATCHSTANDARDofBATCH_CHERRY, met exact hetzelfde optische feature-schema in beide gevallen.
Consistentiecontrole (Anomaliedetectie)
Blind vertrouwen op de batchparameter is riskant; als een afwijkende vrucht in de verkeerde bak belandt, zou het systeem dit anders niet merken. Daarom bevat de pijplijn een cross-check gebaseerd op transit_len — een fysieke meting van de lengte over de sensor die correleert met de grootte van de vrucht:
# src/train_model.py
def check_batch_consistency(batch_type, transit_len,
standard_min_transit_len=12,
cherry_max_transit_len=12):
if batch_type == "standard":
return transit_len > standard_min_transit_len
elif batch_type == "cherry":
return transit_len <= cherry_max_transit_len
Deze drempelwaarden (12 encoderstappen) zijn gerepliceerd in de C-firmware via tomatocheckbatch_anomaly(). De drempel is vastgesteld op 12 omdat geen enkele kerstomaat in de dataset deze waarde overschrijdt. Dit reduceert valse positieven bij standaardtomaten van 23,5% naar 14,6% vergeleken met een drempelwaarde zonder dataverificatie.
Opmerking: Een residu van 14,6% valse positieven blijft bestaan door een fysieke overlap tussen kleine standaardtomaten en grote kerstomaatjes.
Machine Learning en Validatieprotocol
Modelselectie en Hardwarebeperkingen
De inference-engine is een RandomForestClassifier (nestimators=35, maxdepth=6) per batch. Deze is gekozen vanwege de directe vertaalbaarheid naar native C-code zonder afhankelijkheden, wat essentieel is voor microcontrollers met beperkte middelen. De maximale diepte van 6 beperkt het aantal conditionele vertakkingen per boom.
Validatieprotocol
De validatie maakt gebruik van een 5-split GroupKFold, gegroepeerd op tomatoid. Aangezien elke fysieke vrucht slechts één keer wordt gescand, is tomatoid uniek per rij.
Het groeperen per collectiedag was niet toepasbaar omdat 6 van de 8 klassen slechts op één van de twee collectiedagen voorkomen. Het uitsluiten van een volledige dag voor validatie zou de trainingsset voor die specifieke klassen tot nul reduceren. De werkelijke beperking is dat het model voor de meeste klassen nog niet is gevalideerd onder verschillende omgevingsfactoren (licht, kalibratie), waarvoor meer veldcampagnes nodig zijn.
Operationele prestaties (Out-of-Fold, reële data)
| Batch | Monsters | Klassen | Gemiddelde Accuratesse (CV) |
|---|---|---|---|
| STANDARD | 268 | 5 | 77.98% (± 4.03%) |
| CHERRY | 23 | 3 | 100.00% (± 0.00%) |
⚠️ Let op: De cijfers voor de CHERRY-batch moeten met voorzichtigheid worden geïnterpreteerd. Met slechts 23 observaties is de steekproef mogelijk te klein om de accuratesse betrouwbaar in te schatten.
De grootste foutbron in het standaardmodel ligt tussen "Rood-Oranje" en "Rood Standaard". Dit is consistent met het feit dat rijping een continu spectrum is en geen set scherp gescheiden categorieën.
Export van Embedded Firmware (C/C++)
src/exportembedded.py traint beide modellen en genereert één headerbestand (include/tomatoclassifier.h) met:
- Twee namespaced scoring-functies (
scorestandard,scorecherry) — native C-code gegenereerd doorm2cgen. - Een
TomatoBatchModeenum (BATCHSTANDARD,BATCHCHERRY). tomatocheckbatchanomaly(mode, transitlen)voor de consistentiecontrole.predicttomatoclass(mode, input)— een helperfunctie die het juiste model aanroept en altijd het globale klasse-ID (0-7) teruggeeft.
Voorbeeld van firmwaregebruik
TomatoBatchMode current_batch = BATCH_STANDARD; // één keer per shift instellen
// voor elke vrucht in transit:
double input[18] = { /* transit_len, valid_slices, IR1_mean, ... */ };
if (tomato_check_batch_anomaly(current_batch, input[0])) {
// vrucht fysiek incompatibel met de opgegeven batch -> apart afhandelen
} else {
int classe = predict_tomato_class(current_batch, input);
// -> pneumatisch commando op basis van `classe`
}
C vs C++ Compatibiliteit
De functies scorestandard() en scorecherry() gebruiken C99-syntaxis (compound literals). Dit is geldig C, maar geen standaard C++. Een C++ compiler zal dit afwijzen.
Correcte integratie:
- Project volledig in C: Geen actie nodig.
- Project in C++ (bijv. STM32CubeIDE of Arduino/ESP32): Isoleer
tomato_classifier.hin een aparte.ccompilatie-eenheid, compileer deze met de C-compiler en koppel deze aan het C++ project viaextern "C"declaraties.
Een werkend voorbeeld hiervan staat in firmware/tomatoesp32test/. Hier wordt gebruikgemaakt van .cpp in plaats van .ino om de Arduino-preprocessor te omzeilen, wat voorkomt dat prototypes worden gegenereerd voordat custom types (zoals struct TestCase) zijn gedefinieerd.
Stack size op FreeRTOS/ESP32
De functies van m2cgen gebruiken honderden tijdelijke arrays op de stack (~1145 in de huidige header). De standaard loopTask van Arduino-ESP32 heeft slechts 8KB stack, wat leidt tot een stack-overflow. De oplossing is om de inference uit te voeren in een dedicated FreeRTOS-task met een expliciet grotere stack (bijv. 32KB via xTaskCreatePinnedToCore).
Latentie — Gemeten resultaten
In plaats van latentie af te leiden uit de boomdiepte, is er een benchmark uitgevoerd op fysieke ESP32-hardware (gemiddelde over 2000 herhalingen per monster).
| Model | Gemiddelde Latentie | Opmerkingen |
|---|---|---|
| STANDARD (5 klassen) | ~220-550 µs | Bomen altijd op maximale diepte (6) |
| CHERRY (3 klassen) | ~43-49 µs | Veel kortere bomen (gemiddelde diepte 2.77) |
Zelfs in het slechtste geval (~550 µs) is de latentie ruim binnen de marges voor een real-time transportsysteem.
Testen
De tests kunnen worden uitgevoerd met:
pip install -r requirements-dev.txt
python3 -m pytest tests/ -v
Er zijn 19 tests die automatisch draaien bij elke push/PR. Deze testen specifiek op eerder gevonden bugs, zoals:
- Voorkomen dat
is_cherryper ongeluk als training feature wordt gebruikt (data leakage). - Controleren of de consistentiedrempel van 12 encoderstappen 100% van de kerstomaatjes vangt.
- Verifiëren dat de gegenereerde C-header daadwerkelijk compileert met
gcc.
Bekende Beperkingen
- Kleine dataset: Totaal 291 vruchten over slechts 2 dagen; de cherry-batch heeft slechts 23 monsters.
- Onvolledige multi-dag dekking: Het model is voor de meeste klassen niet gevalideerd onder verschillende omgevingscondities (licht, kalibratie).
- Residuele valse positieven: De drempelwaarde van 12 encoderstappen veroorzaakt nog steeds 14,6% valse positieven bij standaardtomaten door fysieke overlap in grootte.
- ESP32 benchmark in "replay" modus: Gemeten latentie betreft alleen de classificatie-inference, niet de volledige acquisitiepijplijn (de fysieke sensor is nog niet aangesloten).
Snelstart en Uitvoeringspijplijn
1. Omgeving voorbereiden
python3 -m venv venv
source venv/bin/activate
pip install --upgrade pip
pip install -r requirements.txt
2. Pijplijn uitvoeren
# 1. Ruwe data laden, opschonen en O(1) features berekenen -> data/processed/
python3 src/data_loader.py
# 2. Training en Out-of-Fold validatie voor BEIDE batches (standard + cherry)
python3 src/train_model.py
# 3. Beide modellen converteren naar één C/C++ header -> include/tomato_classifier.h
python3 src/export_embedded.py
3. Real Hardware Benchmark (ESP32)
Plaats de bestanden uit firmware/tomatoesp32test/ in de src/ en include/ mappen van een PlatformIO project. Voeg de volgende bibliotheken toe aan platformio.ini:
adafruit/Adafruit SSD1306@^2.5.7adafruit/Adafruit GFX Library@^1.11.5
Voer vervolgens uit:
pio run --target upload
pio device monitor # 115200 baud
Groetjes,