""" Create tiny random JPEG datasets under ml/data// so the training pipeline runs without Kaggle. Metrics will not match real leaf data — use this to verify installs, Docker, and end-to-end train → evaluate → TFLite export. Example: python -m ml.scripts.create_synthetic_dataset --crop corn --force python -m ml.training.train_crop --crop corn --epochs 2 --no-fine-tune """ from __future__ import annotations import argparse from pathlib import Path import numpy as np from PIL import Image from ml.config import CROPS, DATA_DIR def _image_extensions() -> tuple[str, ...]: return (".jpg", ".jpeg", ".png", ".JPG", ".JPEG", ".PNG") def _clear_class_folder(folder: Path) -> None: if not folder.is_dir(): return for p in folder.iterdir(): if p.is_file() and p.suffix in _image_extensions(): p.unlink() def write_synthetic_crop( crop: str, images_per_class: int, seed: int, force: bool, image_size: tuple[int, int], ) -> None: if crop not in CROPS: raise ValueError(f"Unknown crop: {crop}") cfg = CROPS[crop] root = DATA_DIR / crop root.mkdir(parents=True, exist_ok=True) rng = np.random.default_rng(seed) diseases = cfg["diseases"] for disease in diseases: folder = root / disease folder.mkdir(parents=True, exist_ok=True) existing = sum(1 for p in folder.iterdir() if p.suffix in _image_extensions()) if existing > 0 and not force: raise SystemExit( f"Refusing to write into non-empty {folder} ({existing} images). " "Use --force to remove existing *.jpg/*.jpeg/*.png in each class folder." ) if force: _clear_class_folder(folder) for i in range(images_per_class): h, w = image_size rgb = rng.integers(0, 256, size=(h, w, 3), dtype=np.uint8) Image.fromarray(rgb, mode="RGB").save( folder / f"synthetic_{i:04d}.jpg", quality=90 ) print(f"Wrote {images_per_class} images → {folder}") def main() -> None: parser = argparse.ArgumentParser( description="Create random RGB image folders for pipeline smoke tests (not real accuracy)." ) parser.add_argument( "--crop", choices=list(CROPS.keys()) + ["all"], default="all", help="Crop to populate (default: all)", ) parser.add_argument( "--images-per-class", type=int, default=48, help="Images per disease folder (default 48; enough for stratified splits)", ) parser.add_argument( "--seed", type=int, default=42, help="RNG seed for reproducible noise images", ) parser.add_argument( "--force", action="store_true", help="Delete existing JPEG/PNG in each class folder before writing", ) args = parser.parse_args() crops = list(CROPS.keys()) if args.crop == "all" else [args.crop] for crop in crops: size = tuple(CROPS[crop]["image_size"]) write_synthetic_crop( crop=crop, images_per_class=args.images_per_class, seed=args.seed, force=args.force, image_size=size, ) print("\nDone. Train with e.g.:") print(" python -m ml.training.train_crop --crop corn --epochs 2 --no-fine-tune") if __name__ == "__main__": main()