| |
| """ |
| prepare_data.py — 将下载的数据集统一为 JSONL 格式 |
| ==================================================== |
| 读取 PlantVillage / PlantDoc / IP102 的目录结构, 生成: |
| {"image_path": "...", "label_en": "...", "label_cn": "...", "crop": "...", "source": "..."} |
| |
| 用法: |
| python prepare_data.py --input-dir data/raw --output-file data/processed/unified_dataset.jsonl |
| """ |
|
|
| from __future__ import annotations |
|
|
| import argparse |
| import json |
| import os |
| import sys |
| from collections import Counter, defaultdict |
| from pathlib import Path |
|
|
| |
| |
| |
| LABEL_CN_MAP: dict[str, str] = { |
| |
| "Tomato___Late_blight": "番茄晚疫病", |
| "Tomato___Early_blight": "番茄早疫病", |
| "Tomato___Bacterial_spot": "番茄细菌性斑点病", |
| "Tomato___Leaf_Mold": "番茄叶霉病", |
| "Tomato___Septoria_leaf_spot": "番茄壳针孢叶斑病", |
| "Tomato___Spider_mites Two-spotted_spider_mite": "番茄二斑叶螨", |
| "Tomato___Target_Spot": "番茄靶斑病", |
| "Tomato___Tomato_Yellow_Leaf_Curl_Virus": "番茄黄化曲叶病毒病", |
| "Tomato___Tomato_mosaic_virus": "番茄花叶病毒病", |
| "Tomato___healthy": "番茄健康", |
| |
| "Apple___Apple_scab": "苹果黑星病", |
| "Apple___Black_rot": "苹果黑腐病", |
| "Apple___Cedar_apple_rust": "苹果雪松锈病", |
| "Apple___healthy": "苹果健康", |
| |
| "Grape___Black_rot": "葡萄黑腐病", |
| "Grape___Esca_(Black_Measles)": "葡萄黑麻疹病", |
| "Grape___Leaf_blight_(Isariopsis_Leaf_Spot)": "葡萄叶枯病", |
| "Grape___healthy": "葡萄健康", |
| |
| "Corn_(maize)___Cercospora_leaf_spot Gray_leaf_spot": "玉米灰斑病", |
| "Corn_(maize)___Common_rust_": "玉米普通锈病", |
| "Corn_(maize)___Northern_Leaf_Blight": "玉米北方叶枯病", |
| "Corn_(maize)___healthy": "玉米健康", |
| |
| "Potato___Early_blight": "马铃薯早疫病", |
| "Potato___Late_blight": "马铃薯晚疫病", |
| "Potato___healthy": "马铃薯健康", |
| |
| "Strawberry___Leaf_scorch": "草莓叶焦病", |
| "Strawberry___healthy": "草莓健康", |
| |
| "Cherry_(including_sour)___Powdery_mildew": "樱桃白粉病", |
| "Cherry_(including_sour)___healthy": "樱桃健康", |
| |
| "Peach___Bacterial_spot": "桃细菌性斑点病", |
| "Peach___healthy": "桃健康", |
| |
| "Pepper,_bell___Bacterial_spot": "辣椒细菌性斑点病", |
| "Pepper,_bell___healthy": "辣椒健康", |
| |
| "Squash___Powdery_mildew": "南瓜白粉病", |
| |
| "Soybean___healthy": "大豆健康", |
| |
| "Raspberry___healthy": "覆盆子健康", |
| |
| "Blueberry___healthy": "蓝莓健康", |
| |
| "Orange___Haunglongbing_(Citrus_greening)": "柑橘黄龙病", |
| |
| "Tomato leaf late blight": "番茄叶片晚疫病", |
| "Tomato leaf early blight": "番茄叶片早疫病", |
| "Tomato leaf bacterial spot": "番茄叶片细菌性斑点病", |
| "Tomato leaf yellow virus": "番茄叶片黄化病毒", |
| "Tomato leaf mosaic virus": "番茄叶片花叶病毒", |
| "Tomato leaf": "番茄叶片健康", |
| "Apple leaf": "苹果叶片健康", |
| "Apple rust leaf": "苹果锈病叶片", |
| "Apple Scab Leaf": "苹果黑星病叶片", |
| "Corn leaf blight": "玉米叶枯病", |
| "Corn rust leaf": "玉米锈病叶片", |
| "Corn Gray leaf spot": "玉米灰斑病", |
| "Potato leaf early blight": "马铃薯早疫病叶片", |
| "Potato leaf late blight": "马铃薯晚疫病叶片", |
| "Grape leaf": "葡萄叶片健康", |
| "Grape leaf black rot": "葡萄叶片黑腐病", |
| "Cherry leaf": "樱桃叶片健康", |
| "Peach leaf": "桃叶片健康", |
| "Raspberry leaf": "覆盆子叶片健康", |
| "Strawberry leaf": "草莓叶片健康", |
| "Soybean leaf": "大豆叶片健康", |
| "Squash Powdery mildew leaf": "南瓜白粉病叶片", |
| "Blueberry leaf": "蓝莓叶片健康", |
| "Bell pepper leaf": "辣椒叶片健康", |
| "Bell pepper leaf spot": "辣椒叶片斑点病", |
| } |
|
|
| |
| CROP_CN_MAP: dict[str, str] = { |
| "tomato": "番茄", |
| "apple": "苹果", |
| "grape": "葡萄", |
| "corn": "玉米", |
| "maize": "玉米", |
| "potato": "马铃薯", |
| "strawberry": "草莓", |
| "cherry": "樱桃", |
| "peach": "桃", |
| "pepper": "辣椒", |
| "bell pepper": "辣椒", |
| "squash": "南瓜", |
| "soybean": "大豆", |
| "raspberry": "覆盆子", |
| "blueberry": "蓝莓", |
| "orange": "柑橘", |
| "citrus": "柑橘", |
| "rice": "水稻", |
| "wheat": "小麦", |
| "cotton": "棉花", |
| } |
|
|
| IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".bmp", ".webp"} |
|
|
|
|
| def _infer_crop(label: str) -> str: |
| """从英文标签推断作物中文名.""" |
| lower = label.lower().replace("_", " ").replace("(", "").replace(")", "") |
| for key, cn in CROP_CN_MAP.items(): |
| if key in lower: |
| return cn |
| return "未知作物" |
|
|
|
|
| def _translate_label(label: str) -> str: |
| """英文标签 → 中文标签, 找不到则返回原文.""" |
| if label in LABEL_CN_MAP: |
| return LABEL_CN_MAP[label] |
| |
| label_spaces = label.replace("_", " ").strip() |
| if label_spaces in LABEL_CN_MAP: |
| return LABEL_CN_MAP[label_spaces] |
| return label |
|
|
|
|
| |
| |
| |
| def process_plantvillage(input_dir: Path) -> list[dict]: |
| """Process PlantVillage: class directories with images inside.""" |
| root = input_dir / "PlantVillage" |
| records = [] |
| if not root.exists(): |
| print(" [PlantVillage] 目录不存在, 跳过") |
| return records |
|
|
| class_dirs = sorted([d for d in root.iterdir() if d.is_dir()]) |
| for class_dir in class_dirs: |
| label_en = class_dir.name |
| label_cn = _translate_label(label_en) |
| crop = _infer_crop(label_en) |
| for img in sorted(class_dir.iterdir()): |
| if img.suffix.lower() in IMAGE_EXTS: |
| records.append({ |
| "image_path": str(img), |
| "label_en": label_en, |
| "label_cn": label_cn, |
| "crop": crop, |
| "source": "PlantVillage", |
| }) |
|
|
| print(f" [PlantVillage] {len(records)} 条记录, {len(class_dirs)} 个类别") |
| return records |
|
|
|
|
| def process_plantdoc(input_dir: Path) -> list[dict]: |
| """Process PlantDoc: look for train/test directories with class subdirectories.""" |
| root = input_dir / "PlantDoc" |
| records = [] |
| if not root.exists(): |
| print(" [PlantDoc] 目录不存在, 跳过") |
| return records |
|
|
| |
| |
| search_roots = [] |
| for candidate in ["train", "test", "Train", "Test", |
| "PlantDoc-Dataset/train", "PlantDoc-Dataset/test"]: |
| p = root / candidate |
| if p.exists(): |
| search_roots.append(p) |
| if not search_roots: |
| |
| search_roots = [root] |
|
|
| seen_paths = set() |
| for sr in search_roots: |
| for class_dir in sorted(sr.iterdir()): |
| if not class_dir.is_dir(): |
| continue |
| label_en = class_dir.name |
| label_cn = _translate_label(label_en) |
| crop = _infer_crop(label_en) |
| for img in sorted(class_dir.iterdir()): |
| if img.suffix.lower() in IMAGE_EXTS and str(img) not in seen_paths: |
| seen_paths.add(str(img)) |
| records.append({ |
| "image_path": str(img), |
| "label_en": label_en, |
| "label_cn": label_cn, |
| "crop": crop, |
| "source": "PlantDoc", |
| }) |
|
|
| print(f" [PlantDoc] {len(records)} 条记录") |
| return records |
|
|
|
|
| def process_ip102(input_dir: Path) -> list[dict]: |
| """Process IP102: numbered class directories (0–101).""" |
| root = input_dir / "IP102" |
| records = [] |
| if not root.exists(): |
| print(" [IP102] 目录不存在, 跳过") |
| return records |
|
|
| |
| classes_file = root / "classes.txt" |
| class_names: dict[str, str] = {} |
| if classes_file.exists(): |
| for line in classes_file.read_text(encoding="utf-8").splitlines(): |
| parts = line.strip().split(maxsplit=1) |
| if len(parts) == 2: |
| class_names[parts[0]] = parts[1] |
|
|
| |
| found_any = False |
| for split in ["train", "val", "test"]: |
| split_dir = root / split |
| if not split_dir.exists(): |
| continue |
| found_any = True |
| for class_dir in sorted(split_dir.iterdir()): |
| if not class_dir.is_dir(): |
| continue |
| class_id = class_dir.name |
| label_en = class_names.get(class_id, f"pest_class_{class_id}") |
| label_cn = _translate_label(label_en) |
| crop = _infer_crop(label_en) |
| for img in sorted(class_dir.iterdir()): |
| if img.suffix.lower() in IMAGE_EXTS: |
| records.append({ |
| "image_path": str(img), |
| "label_en": label_en, |
| "label_cn": label_cn, |
| "crop": crop, |
| "source": "IP102", |
| }) |
|
|
| |
| if not found_any: |
| for class_dir in sorted(root.iterdir()): |
| if not class_dir.is_dir() or not class_dir.name.isdigit(): |
| continue |
| class_id = class_dir.name |
| label_en = class_names.get(class_id, f"pest_class_{class_id}") |
| label_cn = _translate_label(label_en) |
| crop = _infer_crop(label_en) |
| for img in sorted(class_dir.iterdir()): |
| if img.suffix.lower() in IMAGE_EXTS: |
| records.append({ |
| "image_path": str(img), |
| "label_en": label_en, |
| "label_cn": label_cn, |
| "crop": crop, |
| "source": "IP102", |
| }) |
|
|
| print(f" [IP102] {len(records)} 条记录") |
| return records |
|
|
|
|
| |
| |
| |
| def print_statistics(records: list[dict]) -> None: |
| """Print detailed statistics about the unified dataset.""" |
| if not records: |
| print("\n⚠ 没有任何记录, 请先运行 download_datasets.py") |
| return |
|
|
| source_counts = Counter(r["source"] for r in records) |
| crop_counts = Counter(r["crop"] for r in records) |
| label_counts = Counter(r["label_en"] for r in records) |
|
|
| print("\n" + "═" * 60) |
| print("数据统计") |
| print("═" * 60) |
|
|
| print(f"\n 总图片数: {len(records)}") |
|
|
| print("\n ── 按数据集 ──") |
| for src, cnt in source_counts.most_common(): |
| print(f" {src:<20} {cnt:>8}") |
|
|
| print("\n ── 按作物 (前 15) ──") |
| for crop, cnt in crop_counts.most_common(15): |
| print(f" {crop:<20} {cnt:>8}") |
| if len(crop_counts) > 15: |
| print(f" ... 共 {len(crop_counts)} 种作物") |
|
|
| print("\n ── 按类别 (前 20) ──") |
| for label, cnt in label_counts.most_common(20): |
| print(f" {label:<50} {cnt:>6}") |
| if len(label_counts) > 20: |
| print(f" ... 共 {len(label_counts)} 个类别") |
| print() |
|
|
|
|
| |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser( |
| description="将农业图像数据集统一为 JSONL 格式", |
| ) |
| parser.add_argument( |
| "--input-dir", |
| type=str, |
| default="data/raw", |
| help="原始数据集根目录 (default: data/raw)", |
| ) |
| parser.add_argument( |
| "--output-file", |
| type=str, |
| default="data/processed/unified_dataset.jsonl", |
| help="输出 JSONL 文件路径 (default: data/processed/unified_dataset.jsonl)", |
| ) |
| args = parser.parse_args() |
|
|
| input_dir = Path(args.input_dir).resolve() |
| output_file = Path(args.output_file).resolve() |
|
|
| if not input_dir.exists(): |
| print(f"错误: 输入目录不存在: {input_dir}") |
| print("请先运行 download_datasets.py") |
| sys.exit(1) |
|
|
| print(f"输入目录: {input_dir}") |
| print(f"输出文件: {output_file}\n") |
|
|
| |
| all_records: list[dict] = [] |
|
|
| print("━" * 60) |
| print("处理数据集") |
| print("━" * 60) |
|
|
| all_records.extend(process_plantvillage(input_dir)) |
| all_records.extend(process_plantdoc(input_dir)) |
| all_records.extend(process_ip102(input_dir)) |
|
|
| |
| output_file.parent.mkdir(parents=True, exist_ok=True) |
| with open(output_file, "w", encoding="utf-8") as f: |
| for record in all_records: |
| f.write(json.dumps(record, ensure_ascii=False) + "\n") |
|
|
| print(f"\n已写入 {len(all_records)} 条记录到: {output_file}") |
|
|
| |
| print_statistics(all_records) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|