cropintel / ml /scripts /fetch_asdid.py
Jaithra Polavarapu
CropIntel — HF Space deploy (all-in-one app)
889dd1b
Raw
History Blame Contribute Delete
4.66 kB
#!/usr/bin/env python3
"""
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 # px, longest side; training uses 224 so this keeps headroom
# zip name on Zenodo -> class folder name used in ml/config.py CROPS["soybean"].
# ASDID (Auburn, USA) is fully disjoint from the vaishaligbhujade training set
# (India) — the default classes are the ones overlapping our trained labels,
# fetched as the soybean EXTERNAL test set (ml/field_test/soybean).
DEFAULT_CLASSES = {
"healthy": "Healthy",
"frogeye": "Frogeye Leaf Spot",
"target_spot": "Target Leaf Spot",
"soybean_rust": "Rust",
# available but not fetched by default:
# "bacterial_blight": "Bacterial Blight", # NOT bacterial pustule!
# "cercospora_leaf_blight": "Cercospora Leaf Blight",
# "downey_mildew": "Downy Mildew",
# "potassium_deficiency": "Potassium Deficiency",
}
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())