{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "e11a7748",
   "metadata": {},
   "source": [
    "# 实验2：YOLOv8 一阶段水平框检测\n",
    "\n",
    "流程：**读取完整数据 → 构建 YOLO 教学子集 → 数据检查 → 构建数据集 → 构建模型 → 构建训练器 → 开始训练 → 验证与指标 → 测试集推理 → 曲线分析 → 单变量对照**。原始数据只读，训练使用 Notebook 输出的小规模子集。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1 参数与环境准备"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# pathlib：跨平台处理项目路径；json：保存实验清单；os：读取环境变量；\n",
    "# sys：把项目源码加入导入路径；time：测速；platform：记录运行平台。\n",
    "from pathlib import Path\n",
    "import json, os, sys, time, platform\n",
    "\n",
    "ROOT = Path.cwd()\n",
    "while not (ROOT / \"pyproject.toml\").exists() and ROOT != ROOT.parent:\n",
    "    ROOT = ROOT.parent\n",
    "sys.path.insert(0, str(ROOT / \"src\"))\n",
    "from rsdet.notebook_utils import (\n",
    "    set_seed,\n",
    "    build_coco_teaching_subset,\n",
    "    build_yolo_teaching_subset,\n",
    "    build_dota_teaching_subset,\n",
    "    read_yolo_names,\n",
    "    coco_summary,\n",
    "    print_manifest,\n",
    "    find_first_image,\n",
    ")\n",
    "\n",
    "SEED = int(os.getenv(\"SEED\", \"42\"))\n",
    "set_seed(SEED)\n",
    "RUN_TRAINING = bool(int(os.getenv(\"RUN_TRAINING\", \"0\")))\n",
    "# Notebook 读取完整数据源，再写出可追溯的教学子集；原始目录不被覆盖。\n",
    "SUBSET_MAX_IMAGES = int(os.getenv(\"SUBSET_MAX_IMAGES\", \"3000\"))\n",
    "SUBSET_MAX_CLASSES = int(os.getenv(\"SUBSET_MAX_CLASSES\", \"6\"))\n",
    "DATA_ROOT = Path(os.getenv(\"DATA_ROOT\", str(ROOT / \"data\" / \"processed\" / \"dior\")))\n",
    "RUN_ROOT = ROOT / \"runs\" / \"notebook_outputs\"\n",
    "RUN_ROOT.mkdir(parents=True, exist_ok=True)\n",
    "ARTIFACTS = {\"seed\": SEED, \"platform\": platform.platform(), \"python\": platform.python_version()}\n",
    "print({\"root\": str(ROOT), \"run_training\": RUN_TRAINING, \"subset_max_images\": SUBSET_MAX_IMAGES, \"subset_max_classes\": SUBSET_MAX_CLASSES, \"seed\": SEED})\n",
    "\n",
    "def save_json(name, payload):\n",
    "    \"\"\"将阶段性结果写入 runs/notebook_outputs，方便报告引用和复核。\"\"\"\n",
    "    path = RUN_ROOT / name\n",
    "    path.write_text(json.dumps(payload, ensure_ascii=False, indent=2, default=str), encoding=\"utf-8\")\n",
    "    ARTIFACTS[name] = str(path)\n",
    "    return path\n",
    "\n",
    "def plot_history(history, name=\"training_curves.png\"):\n",
    "    \"\"\"读取训练历史并绘制 loss、学习率曲线；无历史时只给出提示。\"\"\"\n",
    "    if not history:\n",
    "        print(\"暂无训练历史；设置 RUN_TRAINING=1 后会保存曲线。\")\n",
    "        return None\n",
    "    try:\n",
    "        import matplotlib.pyplot as plt\n",
    "    except ImportError:\n",
    "        print(\"matplotlib 未安装，跳过曲线绘制。\")\n",
    "        return None\n",
    "    epochs = [row.get(\"epoch\", i + 1) for i, row in enumerate(history)]\n",
    "    fig, axes = plt.subplots(1, 2, figsize=(10, 4))\n",
    "    axes[0].plot(epochs, [row.get(\"loss\", float(\"nan\")) for row in history], marker=\"o\")\n",
    "    axes[0].set_title(\"loss\"); axes[0].set_xlabel(\"epoch\"); axes[0].grid(alpha=.3)\n",
    "    axes[1].plot(epochs, [row.get(\"lr\", float(\"nan\")) for row in history], marker=\"o\", color=\"darkorange\")\n",
    "    axes[1].set_title(\"learning rate\"); axes[1].set_xlabel(\"epoch\"); axes[1].grid(alpha=.3)\n",
    "    fig.tight_layout()\n",
    "    path = RUN_ROOT / name; fig.savefig(path, dpi=150); plt.close(fig)\n",
    "    ARTIFACTS[name] = str(path)\n",
    "    print(\"曲线已保存:\", path)\n",
    "    return path"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Ultralytics 提供 YOLO 模型、训练、验证、推理和导出 API。\n",
    "print(\"Ultralytics 训练建议: ultralytics>=8,<9；记录输入尺寸、batch、epoch、seed 和 GPU。\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2 DIOR 数据准备：DIOR → YOLO 格式"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 从完整 YOLO 目录中选择不超过 SUBSET_MAX_IMAGES 个图像—标签对。\n",
    "SOURCE_ROOT = Path(os.getenv(\"DATASET_ROOT\", str(DATA_ROOT)))\n",
    "SOURCE_IMAGE_DIR = Path(os.getenv(\"IMAGE_DIR\", str(SOURCE_ROOT / \"images\")))\n",
    "SOURCE_LABEL_DIR = Path(os.getenv(\"LABEL_DIR\", str(SOURCE_ROOT / \"labels\")))\n",
    "SOURCE_DATA_YAML = Path(os.getenv(\"DATA_YAML\", str(SOURCE_ROOT / \"data.yaml\")))\n",
    "subset_info = build_yolo_teaching_subset(\n",
    "    SOURCE_IMAGE_DIR, SOURCE_LABEL_DIR, RUN_ROOT / \"yolov8_subset\",\n",
    "    max_images=SUBSET_MAX_IMAGES, seed=SEED,\n",
    ")\n",
    "DATASET_ROOT = Path(subset_info[\"root\"])\n",
    "DATA_YAML = DATASET_ROOT / \"data.yaml\"\n",
    "names = [x.strip() for x in os.getenv(\"YOLO_NAMES\", \"\").split(\",\") if x.strip()] or read_yolo_names(SOURCE_DATA_YAML)\n",
    "DATA_YAML.write_text(\n",
    "    f\"path: {DATASET_ROOT}\\ntrain: images/train\\nval: images/val\\n\"\n",
    "    f\"names: {names!r}\\nnc: {len(names)}\\n\", encoding=\"utf-8\"\n",
    ")\n",
    "ARTIFACTS[\"subset_manifest\"] = str(subset_info[\"manifest\"])\n",
    "print(\"完整 YOLO 数据已处理为教学子集:\", subset_info, \"配置:\", DATA_YAML)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3 数据检查与标签统计"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 标签统计用于发现空标签、类别数量异常和数据路径错误，不改变原始数据。\n",
    "label_files = list((DATASET_ROOT / \"labels\").rglob(\"*.txt\"))\n",
    "label_count = sum(len(p.read_text(encoding=\"utf-8\").splitlines()) for p in label_files)\n",
    "print({\"label_files\": len(label_files), \"objects\": label_count, \"yaml\": str(DATA_YAML)})"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4 构建数据集"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "dataset_root = DATASET_ROOT\n",
    "def pair_count(image_dir, label_dir):\n",
    "    \"\"\"按相对路径检查图像与 YOLO 标签是否一一对应。\"\"\"\n",
    "    image_dir, label_dir = Path(image_dir), Path(label_dir)\n",
    "    image_stems = {p.relative_to(image_dir).with_suffix(\"\") for p in image_dir.rglob(\"*\") if p.suffix.lower() in {\".jpg\", \".jpeg\", \".png\", \".tif\", \".tiff\"}}\n",
    "    label_stems = {p.relative_to(label_dir).with_suffix(\"\") for p in label_dir.rglob(\"*.txt\")}\n",
    "    # 若数据按 train/val/test 分目录，只比较对应 split；避免遗留的根目录标签被误报。\n",
    "    split_names = {\"train\", \"val\", \"test\", \"trainval\"}\n",
    "    image_splits = {key.parts[0] for key in image_stems if key.parts and key.parts[0] in split_names}\n",
    "    if image_splits:\n",
    "        label_stems = {key for key in label_stems if key.parts and key.parts[0] in image_splits}\n",
    "    return {\"images\": len(image_stems), \"labels\": len(label_stems), \"missing_labels\": sorted(image_stems - label_stems)[:10], \"orphan_labels\": sorted(label_stems - image_stems)[:10]}\n",
    "pair_summary = pair_count(dataset_root / \"images\", dataset_root / \"labels\")\n",
    "print(\"图像—标签配对检查:\", pair_summary)\n",
    "if pair_summary[\"missing_labels\"] or pair_summary[\"orphan_labels\"]:\n",
    "    raise ValueError(\"发现图像与标签文件名不匹配，请先修复数据集目录。\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 5 构建模型"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "try:\n",
    "    # YOLO 类封装模型图结构、预训练权重以及 train/val/predict 方法。\n",
    "    from ultralytics import YOLO\n",
    "    model = YOLO(os.getenv(\"YOLO_WEIGHTS\", \"yolov8n.pt\")); print(\"模型构建完成:\", model.model.__class__.__name__ if hasattr(model, \"model\") else \"YOLO\")\n",
    "except ImportError as exc:\n",
    "    YOLO = None; model = None; print(\"Ultralytics 未安装:\", exc)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 6 构建训练器与参数"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 将训练超参数集中保存，便于复现实验和将命令写入报告。\n",
    "TRAIN_KWARGS = {\"data\": str(DATA_YAML), \"epochs\": int(os.getenv(\"EPOCHS\", \"1\")), \"imgsz\": int(os.getenv(\"IMGSZ\", \"640\")), \"batch\": int(os.getenv(\"BATCH\", \"2\")), \"workers\": int(os.getenv(\"WORKERS\", \"0\")), \"seed\": SEED, \"project\": str(RUN_ROOT), \"name\": \"yolov8_train\", \"exist_ok\": True, \"plots\": True}\n",
    "print(\"训练参数:\", TRAIN_KWARGS)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 7 开始训练"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# train_result 保存 Ultralytics 的运行目录；results.csv 用于后续曲线绘制。\n",
    "train_result = None; metrics = None; history = []\n",
    "if model is not None and RUN_TRAINING:\n",
    "    train_result = model.train(**TRAIN_KWARGS)\n",
    "    train_dir = Path(getattr(train_result, \"save_dir\", RUN_ROOT / \"yolov8_train\")); ARTIFACTS[\"train_dir\"] = str(train_dir)\n",
    "    result_csv = train_dir / \"results.csv\"\n",
    "    if result_csv.exists():\n",
    "        import csv\n",
    "        with result_csv.open(encoding=\"utf-8\") as f: history = list(csv.DictReader(f))\n",
    "    save_json(\"yolov8_history.json\", history)\n",
    "else:\n",
    "    print(\"RUN_TRAINING=0 或 Ultralytics 未安装：跳过训练。\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 8 验证集评估与性能指标"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# val() 返回 Precision、Recall、mAP 等验证指标；不把训练 loss 当作检测精度。\n",
    "validation = {}\n",
    "if model is not None and RUN_TRAINING:\n",
    "    metrics = model.val(data=str(DATA_YAML), imgsz=TRAIN_KWARGS[\"imgsz\"], plots=True)\n",
    "    validation = getattr(metrics, \"results_dict\", {})\n",
    "    print(\"验证指标:\", validation)\n",
    "save_json(\"yolov8_metrics.json\", validation)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 9 测试集推理"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 推理使用真实数据目录中的首张图像，仅作为流程示例，不代表模型总体性能。\n",
    "first_image = find_first_image(DATASET_ROOT / \"images\")\n",
    "inference = {\"status\": \"skipped\"}\n",
    "if model is not None and RUN_TRAINING and first_image:\n",
    "    prediction = model.predict(source=str(first_image), conf=.25, save=True, project=str(RUN_ROOT), name=\"yolov8_predict\", exist_ok=True)\n",
    "    inference = {\"image\": str(first_image), \"prediction_count\": len(prediction), \"output\": str(RUN_ROOT / \"yolov8_predict\")}\n",
    "    ARTIFACTS[\"prediction_dir\"] = inference[\"output\"]\n",
    "print(\"推理记录:\", inference); save_json(\"yolov8_inference.json\", inference)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 10 绘制损失、学习率和指标曲线"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "if history:\n",
    "    try:\n",
    "        import matplotlib.pyplot as plt\n",
    "        import pandas as pd\n",
    "        frame = pd.DataFrame(history); numeric = frame.select_dtypes(\"number\")\n",
    "        ax = numeric.plot(subplots=True, figsize=(10, max(4, len(numeric.columns) * 1.8)), grid=True, title=\"YOLOv8 training history\")\n",
    "        fig = ax[0].figure if hasattr(ax, \"__len__\") else ax.figure; fig.tight_layout(); curve = RUN_ROOT / \"yolov8_curves.png\"; fig.savefig(curve, dpi=150); plt.close(fig); ARTIFACTS[\"curve\"] = str(curve)\n",
    "    except ImportError: print(\"pandas/matplotlib 未安装，跳过曲线。\")\n",
    "else: print(\"暂无训练历史；真实训练后将读取 results.csv 绘制曲线。\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 11 单变量对照与实验结论"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"建议对照：保持权重、epoch、batch、数据划分和 seed 不变，仅比较 imgsz=640 与 imgsz=1024。\")\n",
    "print(\"最终产物:\", ARTIFACTS); print_manifest(RUN_ROOT, **ARTIFACTS)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.9+"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
