"""Train the plant-disease and plant-identification CNNs (transfer learning). This produces the model files AgroSense's vision module loads. It needs TensorFlow and the image datasets, so it is meant to be run OUTSIDE the lightweight app sandbox (e.g. Colab / a GPU box) — not in the offline POC environment. Datasets: * Disease : PlantVillage (~54k images, 38 crop-disease classes). Widely mirrored on Kaggle (e.g. "PlantVillage Dataset"). Check the licence of the mirror you use before redistribution. * Plant ID : any labelled leaf/plant dataset arranged as one folder per class (e.g. Flavia leaf dataset, Oxford-102 Flowers, or an MIT-licensed Kaggle plant dataset). Folder layout: dataset//. * Pest : IP102 (~75k images, 102 crop-pest classes) or the Kaggle "Agricultural Pests Image Dataset" (12 classes). Same per-class-folder layout. Usage: pip install "tensorflow>=2.15" python scripts/train_plant_models.py --data path/to/plantvillage --task disease \ --out models/disease.keras --labels models/disease_labels.txt python scripts/train_plant_models.py --data path/to/plantid --task plant \ --out models/plant.keras --labels models/plant_labels.txt python scripts/train_plant_models.py --data path/to/IP102 --task pest \ --out models/pest.keras --labels models/pest_labels.txt Then point AgroSense at them: AGROSENSE_VISION_DISEASE_MODEL=models/disease.keras AGROSENSE_VISION_DISEASE_LABELS=models/disease_labels.txt AGROSENSE_VISION_PLANT_MODEL=models/plant.keras AGROSENSE_VISION_PLANT_LABELS=models/plant_labels.txt The model bakes in resizing+rescaling so the app's inference can feed raw [0,255] 224x224 RGB arrays (matching agrosense/vision.py KerasVisionModel). """ from __future__ import annotations import argparse from pathlib import Path IMG_SIZE = (224, 224) def build_model(num_classes: int): import tensorflow as tf from tensorflow.keras import layers, models base = tf.keras.applications.MobileNetV2( input_shape=IMG_SIZE + (3,), include_top=False, weights="imagenet") base.trainable = False return models.Sequential([ layers.Input(shape=IMG_SIZE + (3,)), layers.Rescaling(1.0 / 127.5, offset=-1.0), # MobileNetV2 expects [-1,1] base, layers.GlobalAveragePooling2D(), layers.Dropout(0.2), layers.Dense(num_classes, activation="softmax"), ]) def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--data", required=True, help="dataset dir (one subfolder per class)") ap.add_argument("--task", choices=["disease", "plant", "pest"], required=True) ap.add_argument("--out", required=True) ap.add_argument("--labels", required=True) ap.add_argument("--epochs", type=int, default=8) ap.add_argument("--batch", type=int, default=32) args = ap.parse_args() import tensorflow as tf train = tf.keras.utils.image_dataset_from_directory( args.data, validation_split=0.2, subset="training", seed=42, image_size=IMG_SIZE, batch_size=args.batch) val = tf.keras.utils.image_dataset_from_directory( args.data, validation_split=0.2, subset="validation", seed=42, image_size=IMG_SIZE, batch_size=args.batch) class_names = train.class_names Path(args.labels).write_text("\n".join(class_names), encoding="utf-8") train = train.prefetch(tf.data.AUTOTUNE) val = val.prefetch(tf.data.AUTOTUNE) model = build_model(len(class_names)) model.compile(optimizer="adam", loss="sparse_categorical_crossentropy", metrics=["accuracy"]) model.fit(train, validation_data=val, epochs=args.epochs) Path(args.out).parent.mkdir(parents=True, exist_ok=True) model.save(args.out) print(f"Saved {args.task} model -> {args.out} ({len(class_names)} classes); " f"labels -> {args.labels}") return 0 if __name__ == "__main__": raise SystemExit(main())