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:

  1. Line-Scan Optische Sensor: Genereert ruwe CSV-data / encoderstappen.
  2. Gegevensverwerking & O(1) Streaming Math (src/dataloader.py): Verwerkt de ruwe data naar een gestructureerde featuretabel, inclusief de aanduiding ischerry per batch.
  3. Training PER BATCH (expliciete batchtype) (src/trainmodel.py):
  • Model STANDAARD: Classificatie in 5 klassen.
  • Model CHERRY: Classificatie in 3 klassen.
  1. Embedded C++ Firmware Vertaling (src/export_embedded.py): Vertaalt beide modellen naar één headerbestand.
  2. 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

IDKlasseBatch TypeResultaat
0Groene standaardtomaatSTANDARDReject (Afkeur)
1Geel-groene tomaatSTANDARDReject (Afkeur)
2Oranje-gele tomaatSTANDARDPass (Acceptatie)
3Rood-oranje tomaatSTANDARDPass (Acceptatie)
4Rode standaardtomaatSTANDARDPass (Acceptatie)
5Gele kerstomaatCHERRYPass (Acceptatie)
6Rode kerstomaatCHERRYPass (Acceptatie)
7Donkergroene gevlekte kerstomaatCHERRYReject (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:

  1. 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.
  2. De firmware-interface is expliciet: de integrator van tomatoclassifier.h declareert één keer per shift BATCHSTANDARD of BATCH_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)

BatchMonstersKlassenGemiddelde Accuratesse (CV)
STANDARD268577.98% (± 4.03%)
CHERRY233100.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 door m2cgen.
  • Een TomatoBatchMode enum (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.h in een aparte .c compilatie-eenheid, compileer deze met de C-compiler en koppel deze aan het C++ project via extern "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).

ModelGemiddelde LatentieOpmerkingen
STANDARD (5 klassen)~220-550 µsBomen altijd op maximale diepte (6)
CHERRY (3 klassen)~43-49 µsVeel 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_cherry per 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.7
  • adafruit/Adafruit GFX Library@^1.11.5

Voer vervolgens uit:

pio run --target upload
pio device monitor # 115200 baud