| |
| """ |
| Fetch the Auburn Soybean Disease Image Dataset (ASDID) from Zenodo. |
| |
| ASDID (Zenodo record 7304859, Dryad doi:10.5061/dryad.41ns1rnj3) is ~9,981 |
| field images where healthy and diseased leaves come from ONE acquisition |
| program — unlike the previous soybean training data, whose Healthy class came |
| from a different source than the disease classes (the model learned to detect |
| the source, not the disease). |
| |
| The class zips are full-resolution (2.8–8.4 GB each, ~35 GB total), far more |
| than training needs. To stay within limited disk space this script processes |
| one class at a time: download zip -> resize images straight out of the zip |
| (max side RESIZE_MAX px, JPEG quality 85) into the output folder -> delete |
| the zip. Peak transient disk usage is one zip (max 8.4 GB). |
| |
| Usage: |
| python -m ml.scripts.fetch_asdid # all default classes |
| python -m ml.scripts.fetch_asdid --classes healthy frogeye |
| python -m ml.scripts.fetch_asdid --out ml/data/soybean/data |
| """ |
| import argparse |
| import io |
| import subprocess |
| import sys |
| import zipfile |
| from pathlib import Path |
|
|
| from PIL import Image |
|
|
| ROOT = Path(__file__).resolve().parents[2] |
| ZENODO_URL = "https://zenodo.org/records/7304859/files/{name}.zip?download=1" |
| IMG_EXTS = {".jpg", ".jpeg", ".png", ".bmp", ".webp"} |
| RESIZE_MAX = 600 |
|
|
| |
| |
| |
| |
| DEFAULT_CLASSES = { |
| "healthy": "Healthy", |
| "frogeye": "Frogeye Leaf Spot", |
| "target_spot": "Target Leaf Spot", |
| "soybean_rust": "Rust", |
| |
| |
| |
| |
| |
| } |
|
|
|
|
| def process_zip(zip_path: Path, out_dir: Path) -> int: |
| """Resize every image in the zip into out_dir; returns count written.""" |
| out_dir.mkdir(parents=True, exist_ok=True) |
| written = 0 |
| with zipfile.ZipFile(zip_path) as zf: |
| for info in zf.infolist(): |
| name = Path(info.filename) |
| if info.is_dir() or name.suffix.lower() not in IMG_EXTS: |
| continue |
| if name.name.startswith("._") or "__MACOSX" in info.filename: |
| continue |
| out_path = out_dir / f"{name.stem}.jpg" |
| if out_path.exists(): |
| continue |
| try: |
| img = Image.open(io.BytesIO(zf.read(info))).convert("RGB") |
| except Exception as e: |
| print(f" [skip] {info.filename}: {e}") |
| continue |
| if max(img.size) > RESIZE_MAX: |
| img.thumbnail((RESIZE_MAX, RESIZE_MAX), Image.LANCZOS) |
| img.save(out_path, "JPEG", quality=85) |
| written += 1 |
| return written |
|
|
|
|
| def fetch_class(zip_name: str, class_dir: Path, tmp_dir: Path) -> int: |
| zip_path = tmp_dir / f"{zip_name}.zip" |
| url = ZENODO_URL.format(name=zip_name) |
| print(f"\n=== {zip_name} -> {class_dir}") |
| print(f" downloading {url}") |
| subprocess.run( |
| ["curl", "-L", "--fail", "--retry", "3", "-C", "-", "-o", str(zip_path), url], |
| check=True, |
| ) |
| try: |
| n = process_zip(zip_path, class_dir) |
| print(f" wrote {n} images") |
| finally: |
| zip_path.unlink(missing_ok=True) |
| return n |
|
|
|
|
| def main(): |
| ap = argparse.ArgumentParser(description=__doc__, |
| formatter_class=argparse.RawDescriptionHelpFormatter) |
| ap.add_argument("--out", default=str(ROOT / "ml" / "data" / "soybean_asdid"), |
| help="output root; one subfolder per class") |
| ap.add_argument("--classes", nargs="*", default=list(DEFAULT_CLASSES.keys()), |
| help=f"zip names to fetch (default: {list(DEFAULT_CLASSES.keys())})") |
| ap.add_argument("--tmp", default="/tmp/asdid", help="scratch dir for zips") |
| args = ap.parse_args() |
|
|
| out_root = Path(args.out) |
| tmp_dir = Path(args.tmp) |
| tmp_dir.mkdir(parents=True, exist_ok=True) |
|
|
| total = 0 |
| for zip_name in args.classes: |
| class_name = DEFAULT_CLASSES.get(zip_name, zip_name) |
| total += fetch_class(zip_name, out_root / class_name, tmp_dir) |
| print(f"\nDone. {total} images under {out_root}") |
| return 0 |
|
|
|
|
| if __name__ == "__main__": |
| sys.exit(main()) |
|
|