A TensorFlow/Keras project for classifying fruits and vegetables using the Fruits-360 dataset. Trains and evaluates MobileNetV2 and EfficientNetB0 models with proper per-model preprocessing.
Tensorflow/
├── model.ipynb # Complete training notebook (Google Colab) — 17 code cells + markdown headers
├── README.md
├── .gitignore
├── .gitattributes # Git LFS tracking for model artifacts
├── models/ # Trained artifacts (Git LFS tracked)
│ ├── final_mobilenet.keras
│ ├── final_efficientnet.keras
│ ├── mobilenet_model.tflite
│ ├── efficientnet_model.tflite
│ ├── labels.txt
│ └── results.json
├── CropIQ/ # Dataset subset (Test/Validation images)
│ ├── README.md # Fruits-360 dataset documentation
│ ├── Test/
│ └── Validation/
Fruits-360 (Version 2026.5.12.0) - 102,551 images across 145 classes (fruits, vegetables, nuts, seeds).
This project uses a 13-class subset + background:
- Apple, Banana, Cabbage, Carrot, Cucumber, Eggplant, Grape, Onion, Orange, Papaya, Pepper, Strawberry, Tomato
- Background (from COCO unlabeled2017)
Split: 50% Training / 25% Validation / 25% Test (preserves original Fruits-360 specimen-level splits)
| Model | Preprocessing | Parameters |
|---|---|---|
| MobileNetV2 | mobilenet_v2.preprocess_input (→ [-1, 1]) |
~3.5M |
| EfficientNetB0 | efficientnet.preprocess_input (→ [0, 255] internal) |
~5.3M |
-
Per-model preprocessing (critical): Both models now receive raw [0, 255] images from generators. Each model has a
Lambdapreprocessing layer:- MobileNetV2: scales to [-1, 1] via
mobilenet_v2.preprocess_input - EfficientNetB0: passes through unchanged; model's internal layers handle scaling
- MobileNetV2: scales to [-1, 1] via
-
Removed
rescale=1./255from allImageDataGeneratorinstances — was causing double-scaling for EfficientNetB0 -
BatchNorm freezing during fine-tuning: BN running statistics frozen (
layer.trainable = False) on the frozen stem to prevent corruption; BN inside the unfrozen window is trained (batch size 64 keeps stats stable) -
Original dataset splits preserved: Uses Fruits-360's original Training/Validation/Test folders (specimen-level split by k, k+1, k+2, k+3 rule), not random image-level shuffle
-
Folder→class mapping fixed: Merges Fruit-360 variety folders into their parent classes via prefix matching (e.g.
Apple Braeburn→Apple) instead of exact-name matching, which was silently dropping all suffixed varieties (Apple, Grape, Onion, Pepper had zero images) -
Background replacement as a gated layer:
RandomBackgroundReplaceis aLayersubclass with atrainingargument — augmentation runs only duringfit(), passes through at inference, and keepstf.random.uniformout of the exported TFLite graph (was previously breakingconverter.convert())
- Mixed precision (
mixed_float16): ~2x faster training on T4 with lower VRAM usage - Batch size 64: more stable BatchNorm stats, better GPU utilization
- Callbacks on
val_accuracy(notval_loss): early stopping + best-weights checkpointing align with the actual goal;TerminateOnNaNguards against divergence - GPU memory growth + device detection at startup
- Ensemble evaluation: results.json includes averaged-probability ensemble accuracy (typically +1-3% over best single model)
Target accuracy: 90-95% on the test set (single models); the ensemble in results.json typically lands at the top of that range.
- Phase 1 (Head): 20 epochs, LR 1e-3 → 1e-5 (cosine decay + warmup)
- Phase 2 (Fine-tune last 30 layers): 20 epochs, LR 5e-5 → 1e-6
- Phase 1 (Head): 20 epochs, LR 1e-3 → 1e-5
- Phase 2 (Fine-tune last 30 layers): 25 epochs, LR 5e-5 → 1e-7
Shared: AdamW (weight_decay=1e-4), Label Smoothing (0.1), Class Weights (balanced), Top-3 Accuracy metric, mixed float16
pip install tensorflow opencv-python scikit-learn matplotlib tqdm- Open
model.ipynbin Google Colab (GPU runtime) - Upload
CropIQ.zipto Drive:MyDrive/CropIQ/CropIQ.zip - Run cells top-to-bottom (17 code cells with markdown headers) — handles extraction, class merging, background download, training both models, evaluation, ensemble, and export
- Models saved to Drive:
best_mobilenet.keras,best_efficientnet.keras(+_finetunedvariants) - TFLite exports:
mobilenet_model.tflite,efficientnet_model.tflite,labels.txt - Results file:
results.json— contains test accuracy, per-class precision/recall/F1, confusion matrices, training history, inference benchmarks, and ensemble accuracy for both models.
Trained artifacts are stored in models/ (Git LFS tracked):
# After training, copy from Drive to repo:
cp "/content/drive/MyDrive/CropIQ/*.keras" models/
cp "/content/drive/MyDrive/CropIQ/*.tflite" models/
cp "/content/drive/MyDrive/CropIQ/labels.txt" models/
cp "/content/drive/MyDrive/CropIQ/results.json" models/Then commit (Git LFS handles large files):
git add models/
git commit -m "Add trained models + artifacts"
git pushFor the CropIQ Android app, copy only the TFLite model + labels to app/src/main/assets/:
app/src/main/assets/
├── mobilenet_model.tflite (or efficientnet_model.tflite)
└── labels.txt
Use the provided CropIQClassifier Kotlin class (supports GPU/NNAPI delegates, Flex ops).
Active development on cropiq-android-integration branch:
git checkout cropiq-android-integration- Background domain shift: Training uses random background augmentation; validation/test use original white studio backgrounds. Real-world deployment (phone camera) will have varied backgrounds.
- External test set needed: 100% accuracy on studio photos ≠ real-world performance. Collect phone photos for true evaluation.
- Eggplant class has 0 samples in current dataset — model cannot predict it. Add Eggplant folders to CropIQ.zip or remove from
TARGET_CLASSES. - TFLite models require Flex delegate (
SELECT_TF_OPS) due to mixed-precision training. For pure TFLite, retrain withfloat32policy.
Dataset: CC BY-SA 4.0 (Mihai Oltean, Fruits-360) Code: MIT License