| """ |
| Import a fixed number of validated soybean healthy images into ml/data/soybean_healthy/Healthy. |
| |
| Uses shutil.move on the same volume to avoid duplicating large files when disk is tight. |
| Skips unreadable/corrupt images (common after partial zip extraction). |
| |
| Example: |
| python -m ml.scripts.import_soybean_healthy --limit 500 --delete-diseased-sibling --delete-source-after |
| """ |
| from __future__ import annotations |
|
|
| import argparse |
| import shutil |
| import sys |
| from pathlib import Path |
|
|
| from PIL import Image |
|
|
| from ml.config import DATA_DIR |
|
|
|
|
| def _image_paths(folder: Path) -> list[Path]: |
| exts = {".jpg", ".jpeg", ".png", ".JPG", ".JPEG", ".PNG"} |
| return sorted( |
| p for p in folder.iterdir() if p.is_file() and p.suffix in exts |
| ) |
|
|
|
|
| def _is_valid_image(path: Path) -> bool: |
| try: |
| with Image.open(path) as im: |
| im.verify() |
| with Image.open(path) as im: |
| im.convert("RGB") |
| return True |
| except OSError: |
| return False |
|
|
|
|
| def main() -> int: |
| parser = argparse.ArgumentParser(description="Import soybean healthy images for training") |
| parser.add_argument( |
| "--source", |
| type=Path, |
| default=Path.home() |
| / "Soybean Healthy and Diseased Images Dataset" |
| / "Soybean Healthy", |
| help="Folder containing Mendeley 'Soybean Healthy' jpegs", |
| ) |
| parser.add_argument( |
| "--dest", |
| type=Path, |
| default=DATA_DIR / "soybean_healthy" / "Healthy", |
| help="Drop-in folder read by CropDatasetLoader", |
| ) |
| parser.add_argument("--limit", type=int, default=500, help="Max images to move") |
| parser.add_argument( |
| "--delete-diseased-sibling", |
| action="store_true", |
| help="Remove ../Soybean Diseased (frees disk; user asked to drop non-healthy)", |
| ) |
| parser.add_argument( |
| "--delete-source-after", |
| action="store_true", |
| help="After moving --limit images, delete remaining files in --source", |
| ) |
| args = parser.parse_args() |
|
|
| source: Path = args.source.expanduser() |
| dest: Path = args.dest.expanduser() |
|
|
| if args.delete_diseased_sibling: |
| diseased = source.parent / "Soybean Diseased" |
| if diseased.is_dir(): |
| print(f"Removing diseased folder: {diseased}") |
| shutil.rmtree(diseased) |
|
|
| if not source.is_dir(): |
| print(f"Source not found: {source}", file=sys.stderr) |
| return 1 |
|
|
| dest.mkdir(parents=True, exist_ok=True) |
|
|
| moved = 0 |
| skipped = 0 |
| for path in _image_paths(source): |
| if moved >= args.limit: |
| break |
| if not _is_valid_image(path): |
| print(f"Skip corrupt/unreadable: {path.name}") |
| skipped += 1 |
| continue |
| target = dest / path.name |
| if target.exists(): |
| stem, suf = path.stem, path.suffix |
| n = 1 |
| while target.exists(): |
| target = dest / f"{stem}_{n}{suf}" |
| n += 1 |
| shutil.move(str(path), str(target)) |
| moved += 1 |
|
|
| print(f"Moved {moved} images to {dest} (skipped {skipped} corrupt).") |
|
|
| if moved < args.limit: |
| print( |
| f"Warning: only {moved} valid images available (wanted {args.limit}).", |
| file=sys.stderr, |
| ) |
|
|
| if args.delete_source_after: |
| for path in _image_paths(source): |
| path.unlink(missing_ok=True) |
| for extra in source.iterdir(): |
| if extra.is_file(): |
| extra.unlink(missing_ok=True) |
| try: |
| source.rmdir() |
| except OSError: |
| pass |
| parent = source.parent |
| if parent.is_dir() and not any(parent.iterdir()): |
| parent.rmdir() |
|
|
| return 0 |
|
|
|
|
| if __name__ == "__main__": |
| raise SystemExit(main()) |
|
|