Chili Ripeness Model with YOLOv11

Chili Ripeness Model with YOLOv11

A YOLOv11 detector trained from scratch to find every chili on a plant and grade it unripe, turning, ripe or broken, small enough to run on a Raspberry Pi 4.

The gantry can put a camera over any plant, but a photo of a chili bush is a hard thing to read: dozens of fruits per frame, half of them hidden behind leaves, and green fruit that's almost the same colour as the foliage around it. Classifying cropped single fruits, which most earlier chili studies did, doesn't help here. The model has to find every fruit in a busy scene and grade each one.

This is the vision half of the thesis. The dataset was collected and labelled for the project, the model was trained from random weights with no pretrained backbone, it was tuned over four runs and benchmarked against YOLOv8s and Faster R-CNN. It then ran for a week on the Raspberry Pi in the greenhouse, where it was checked against a hand count of every fruit.

Results

0.855

mAP@50 on the held-out test set

0.838

Precision (recall 0.771, mAP@50-95 0.593)

170 ms

Per image on a Raspberry Pi 4 (~5.5 FPS)

94.3%

Precision against a hand count in the greenhouse

Four  classes

Unripe

Light to dark green, small and firm. No damage, just not ready yet.

Turning

Partway from green to orange or red, skin starting to soften. The in-between stage.

Ripe

Evenly orange to bright red, full-sized, smooth and glossy. Ready to pick.

Broken

Brown, black or spotted; soft, wrinkled or holed. Judged against the Indonesian national standard (SNI) for fresh chili, whatever the cause.

Building  the  dataset

Every image comes from a camera pointed at real plants. 526 were shot in a home garden where the plants were raised, from straight-on and angled-down viewpoints in the morning, midday and afternoon. 208 came from the gantry's own top-down camera in the greenhouse, including evening shots. Healthy plants rarely have rotten fruit, so 195 images from a public Roboflow set of diseased chilies filled in the broken class.

Footage was recorded as 1080p video at 30 fps and sampled at two frames per second, then cleaned of blurry, duplicate and chili-free frames. That left 929 images, labelled by hand in Roboflow with 28,263 bounding boxes: about 30 fruits per image. The classes are far from balanced (16,907 unripe, 8,209 ripe, 2,767 turning, 380 broken), which is simply what a plant looks like mid-season.

The images were split 70/20/10 into 650 training, 186 validation and 93 test images, and the training set was tripled to 1,950 with 90° rotations, light noise and horizontal flips.

A labelled frame with dozens of unripe, turning and ripe boxes
A rotting chili on the plant
Broken fruit labelled in Roboflow

Four  tuning  runs  (mAP@50)

0.780

Run 1: YOLOv11n at 640 px, default SGD

0.812

Run 2: same model at 960 px

0.824

Run 3: AdamW with a cosine learning-rate schedule

0.858

Run 4: YOLOv11s at 1280 px, tighter regularisation

What  each  run  fixed

Run 1 was a baseline with Ultralytics defaults. Its mAP@50 and mAP@50-95 sat 0.285 apart, a sign of sloppy boxes: at 640 px a single frame packs 40-plus fruits into very few pixels each. Raising the resolution to 960 px in Run 2 lifted both numbers, but the curves plateaued by epoch 123 with the learning rate stuck where it started.

Run 3 swapped in AdamW at a lower learning rate with cosine decay and a longer warmup. Training stayed stable to the last epoch, but gained only 0.012. That pointed at the nano model's capacity, not the optimiser, as the limit.

Run 4 moved to YOLOv11s at 1280 px and tightened regularisation (more weight decay, mixup, a heavier classification loss, a longer warmup and close-mosaic window) so the bigger network wouldn't overfit while learning from scratch. It peaked between epochs 108 and 127 and early-stopped at 177, after about 4 hours on two Tesla T4s on Kaggle.

Final  configuration

  • YOLOv11s, from scratch
  • 1280 px input
  • Batch 16
  • AdamW, lr0 0.005
  • Cosine LR
  • Warmup 8 epochs
  • Weight decay 0.0007
  • cls loss 0.7
  • Mixup 0.1
  • close_mosaic 20
  • Patience 50
  • NMS IoU 0.45
Run 4 training and validation losses, precision, recall and mAP
Normalised confusion matrix of the final model
F1-confidence curve peaking at 0.80 at confidence 0.489

mAP@50  by  class

0.900

Broken

0.895

Ripe

0.883

Unripe

0.743

Turning

The  turning  problem

Three classes are solid. Turning isn't. The confusion matrix gets it right 71% of the time, with the rest mostly lost to background or called unripe: a fruit that's half green and half orange sits between two classes and, from above, looks a lot like the leaves around it. The background row shows the same thing in reverse, with leaves, stems and shadows mistaken for unripe fruit.

The F1-confidence curve sets the deployed threshold. F1 peaks at 0.80 at a confidence of 0.489 and stays flat between about 0.1 and 0.7. Precision only reaches 1.0 at 0.959, by which point recall has collapsed, so 0.489 is what the Pi uses.

YOLOv8s and Faster R-CNN, trained on the same data, both show the same weakness, at 0.726 and 0.600 mAP@50 for turning. That points at the data, not the architecture.

Stress  tests

Before going near the hardware, the model was checked image by image, with every fruit counted by hand. Six scenes covered a top-down view in the greenhouse, a side-on view at plant height, a close-up, a plant outside the greenhouse, a dense frame from the gantry camera and a frame built around rotten fruit.

Across 257 detections it scored 94.9% precision and 87.5% recall. Side-on views hurt recall most (82.1%), since leaves hide more fruit. The gantry view caught every fruit but over-detected a little (85.3% precision), and the outside-the-greenhouse plant scored 95.8% on both, so the model isn't tied to one setting.

Top-down test image before detection
The same image with YOLOv11 detections

Against  the  alternatives

0.855

YOLOv11s mAP@50, 170 ms per image on the Pi

0.845

YOLOv8s mAP@50, 195 ms per image on the Pi

0.734

Faster R-CNN (ResNet-50 FPN) mAP@50, 1,650 ms per image

9.7×

How much faster YOLOv11s runs than Faster R-CNN on the Pi

Why  YOLOv11

All three were trained from scratch on the same data and hardware, with hyperparameters matched as closely as each architecture allowed. YOLOv8s came close: slightly higher recall (0.799), slightly lower mAP across the board, and 25 ms slower on the Pi despite having more parameters (about 11.2M against 9.4M).

Faster R-CNN was the outlier. Each epoch took about 372 seconds against roughly 80 for the YOLO models, and the run hit Kaggle's 12-hour session cap at epoch 106. Its precision of 0.560 against a recall of 0.796 shows a model that over-predicts, and at 1.65 s per image on the Pi it's too slow for a scan that already spends its time moving.

Faster R-CNN's normalised confusion matrix

In  the  field

The real test was a week of scans in the greenhouse, 1 to 7 July 2026. Before a scan, every fruit on all 27 plants was counted by hand: 950 fruits in total. The model found 863 of them, with 52 false detections and 87 misses, for 94.3% precision and 90.8% recall. Misses outnumber false alarms, which fits what the camera sees: fruit tucked behind leaves.

Ripe and unripe both held above 95% precision and 91% recall. Turning dropped to 75.4% and 77.8%, the same weak spot as on the test set. Over six complete sessions the class mix barely moved from day to day, with unripe making up about two thirds of detections and ripe about 30%.

The harvest rule (more than five ripe fruits means pick it) matched the hand count on 25 of 27 plants. Both misses were false alarms on plants sitting right at the threshold, where one wrong ripe call flips the decision.

Next

Turning needs more examples, plus augmentation aimed at that colour range, such as targeted colour jitter and hard-negative mining on the fruit it confuses with ripe and unripe. The broken class should be rebuilt from real greenhouse photos instead of a borrowed dataset. And midday light, which washes out the colours and is why every scan ran in the late afternoon, needs either training data from those hours or a diffuser on the camera.

Built  with

  • Ultralytics YOLOv11
  • PyTorch
  • Roboflow
  • Kaggle (2× Tesla T4)
  • OpenCV
  • Python
  • Raspberry Pi 4

Related  Work