{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/home/worker/anaconda3/envs/250520_mmyolo/lib/python3.8/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n",
      "  from .autonotebook import tqdm as notebook_tqdm\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "① COCO JSON 로드 성공 (/data/ltb/ultralytics/yolo11_datasets/objects365/zhiyuan_objv2_val.json)\n",
      "✅ 클래스 365개 확보 완료  ← /data/ltb/ultralytics/yolo11_datasets/objects365/zhiyuan_objv2_val.json\n"
     ]
    }
   ],
   "source": [
    "# ===============================================================\n",
    "# [Cell 1] 경로 · 옵션 · Objects365 클래스 안전 로딩 (JSON 우선)\n",
    "# ===============================================================\n",
    "from pathlib import Path\n",
    "import sys, json, importlib.util, cv2, shutil, os, random\n",
    "from collections import defaultdict\n",
    "from tqdm.auto import tqdm\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "# ----------------------------------------------------------------\n",
    "# 0) 기본 경로 -----------------------------------------------------\n",
    "YOLO_ROOT   = Path(\"/data/ltb/ultralytics/yolo11_datasets/objects365\")\n",
    "OUTPUT_ROOT = Path(\"/data/ltb/ultralytics/objects365_mmyolo_coco\")\n",
    "\n",
    "IMG_OUT_DIR  = OUTPUT_ROOT / \"images\"\n",
    "ANNOT_TRAIN  = OUTPUT_ROOT / \"train_coco.json\"\n",
    "ANNOT_VAL    = OUTPUT_ROOT / \"val_coco.json\"\n",
    "DEBUG_OUTDIR = OUTPUT_ROOT / \"debug_vis\"\n",
    "for d in (IMG_OUT_DIR, DEBUG_OUTDIR):\n",
    "    d.mkdir(parents=True, exist_ok=True)\n",
    "\n",
    "# ----------------------------------------------------------------\n",
    "# 1) 클래스 소스 우선순위 ------------------------------------------\n",
    "JSON_PATH   = YOLO_ROOT / \"zhiyuan_objv2_val.json\"            # 1순위\n",
    "CUSTOM_PY   = Path(\"/data/ltb/mmyolo/objects365_classes.py\")  # 2순위\n",
    "\n",
    "def load_from_json(coco_json: Path):\n",
    "    data = json.loads(coco_json.read_text(encoding=\"utf-8\"))\n",
    "    cats = sorted(data[\"categories\"], key=lambda c: c[\"id\"])\n",
    "    names = [c[\"name\"] for c in cats]\n",
    "    return names\n",
    "\n",
    "def load_from_py(py_file: Path):\n",
    "    spec = importlib.util.spec_from_file_location(\"obj365_tmp\", py_file)\n",
    "    mod  = importlib.util.module_from_spec(spec)\n",
    "    spec.loader.exec_module(mod)\n",
    "    return getattr(mod, \"OBJECTS365_CLASSES\", [])\n",
    "\n",
    "def load_official_meta():\n",
    "    try:\n",
    "        from mmdet.datasets import Objects365Dataset as _DS\n",
    "        return list(_DS.METAINFO[\"classes\"])\n",
    "    except Exception:\n",
    "        # 최후 백업 365 리스트 (필요 시 채워 사용)\n",
    "        return [\n",
    "            \"air conditioner\",\"airplane\",\"alarm clock\",\"alligator\",\"almond\",\"ambulance\",\n",
    "            # … (중략) …\n",
    "            \"wrench\",\"yak\",\"zebra\",\"zigzag\",\"zucchini\"\n",
    "        ]\n",
    "\n",
    "# ---------- 실제 로딩 ----------\n",
    "origin, cls_list = None, []\n",
    "# ① JSON\n",
    "if JSON_PATH.exists():\n",
    "    try:\n",
    "        cls_list = load_from_json(JSON_PATH)\n",
    "        origin   = str(JSON_PATH)\n",
    "        print(f\"① COCO JSON 로드 성공 ({origin})\")\n",
    "    except Exception as e:\n",
    "        print(f\"   • JSON 파싱 실패 → {e}\")\n",
    "\n",
    "# ② 커스텀 모듈\n",
    "if not cls_list and CUSTOM_PY.exists():\n",
    "    try:\n",
    "        cls_list = load_from_py(CUSTOM_PY)\n",
    "        origin   = str(CUSTOM_PY)\n",
    "        print(f\"② 커스텀 모듈 로드 ({origin})\")\n",
    "    except Exception as e:\n",
    "        print(f\"   • 커스텀 모듈 실패 → {e}\")\n",
    "\n",
    "# ③ 일반 모듈 import\n",
    "if not cls_list:\n",
    "    try:\n",
    "        import objects365_classes as _m\n",
    "        cls_list = _m.OBJECTS365_CLASSES\n",
    "        origin   = str(Path(_m.__file__).resolve())\n",
    "        print(f\"③ 시스템 모듈 로드 ({origin})\")\n",
    "    except ModuleNotFoundError:\n",
    "        pass\n",
    "\n",
    "# ④ 공식 메타 or 백업\n",
    "if not cls_list:\n",
    "    cls_list = load_official_meta()\n",
    "    origin   = \"mmdet METAINFO / 백업 리스트\"\n",
    "    print(\"④ 공식 메타/백업 리스트 사용\")\n",
    "\n",
    "# ---------- 검증 & 보고 ----------\n",
    "if len(cls_list) == 365:\n",
    "    print(f\"✅ 클래스 365개 확보 완료  ← {origin}\")\n",
    "else:\n",
    "    print(f\"⚠️  로드된 클래스 수 = {len(cls_list)} (정상 365)\\n\"\n",
    "          f\"   • 소스: {origin}\\n\"\n",
    "          \"   • 부족/초과 항목 자동 보정: \"\n",
    "          \"Category_000~ 형식으로 채워 사용합니다.\")\n",
    "    # 과부족 보정\n",
    "    cls_list = (cls_list + [f\"Category_{i:03d}\" for i in range(365)])[:365]\n",
    "\n",
    "dup = len(cls_list) - len(set(cls_list))\n",
    "if dup:\n",
    "    print(f\"⚠️  중복 클래스 {dup}개 존재 (변환은 그대로 진행)\")\n",
    "\n",
    "CLASS_NAMES = {i: n for i, n in enumerate(cls_list)}\n",
    "\n",
    "# ----------------------------------------------------------------\n",
    "# 2) 변환 옵션 -----------------------------------------------------\n",
    "RANDOM_SEED   = 42\n",
    "DEBUG_VIS     = True\n",
    "DEBUG_CNT     = 30\n",
    "LINK_METHOD   = \"hardlink\"   # hardlink | symlink | copy\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "# ===============================================================\n",
    "# [Cell 2] 유틸 함수\n",
    "# ===============================================================\n",
    "def load_yolo_labels(txt_path: Path):\n",
    "    \"\"\"YOLO txt 1장 → [(cls, cx, cy, w, h), ...]\"\"\"\n",
    "    rec = []\n",
    "    for ln in txt_path.read_text().strip().splitlines():\n",
    "        p = ln.split()\n",
    "        if len(p) == 5:\n",
    "            c, xc, yc, w, h = map(float, p)\n",
    "            rec.append((int(c), xc, yc, w, h))\n",
    "    return rec\n",
    "\n",
    "def yolo2coco_bbox(xc, yc, bw, bh, img_w, img_h):\n",
    "    \"\"\"YOLO norm → COCO abs [x_min, y_min, w, h]\"\"\"\n",
    "    abs_w, abs_h = bw * img_w, bh * img_h\n",
    "    return [(xc * img_w) - abs_w / 2,\n",
    "            (yc * img_h) - abs_h / 2,\n",
    "             abs_w, abs_h]\n",
    "\n",
    "def next_id(counter, key):\n",
    "    counter[key] += 1\n",
    "    return counter[key]\n",
    "\n",
    "def link_or_copy(src: Path, dst: Path):\n",
    "    \"\"\"\n",
    "    LINK_METHOD(전역) 순서에 따라 하드링크/심링크/카피 수행.\n",
    "    cross-device 오류 등으로 실패하면 자동 폴백.\n",
    "    \"\"\"\n",
    "    if dst.exists():\n",
    "        return\n",
    "    method_seq = {\n",
    "        \"hardlink\": [\"hardlink\", \"symlink\", \"copy\"],\n",
    "        \"symlink\" : [\"symlink\", \"copy\"],\n",
    "        \"copy\"    : [\"copy\"],\n",
    "    }[LINK_METHOD]\n",
    "\n",
    "    for m in method_seq:\n",
    "        try:\n",
    "            if m == \"hardlink\":\n",
    "                os.link(src, dst)\n",
    "            elif m == \"symlink\":\n",
    "                os.symlink(src, dst)\n",
    "            else:\n",
    "                shutil.copy2(src, dst)\n",
    "            return\n",
    "        except OSError as e:\n",
    "            # cross-device link 등 오류 → 다음 방법 시도\n",
    "            last_err = e\n",
    "    raise RuntimeError(f\"{src} → {dst} 링크/복사 실패: {last_err}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "# ===============================================================\n",
    "# [Cell 3] split(train/val) → COCO 변환\n",
    "# ===============================================================\n",
    "def convert_split(split:str, json_out:Path,\n",
    "                  img_offset:int=0, ann_offset:int=0):\n",
    "    images, annots = [], []\n",
    "    cnt = defaultdict(int, img=img_offset, ann=ann_offset)\n",
    "\n",
    "    img_dir = YOLO_ROOT / \"images\" / split\n",
    "    lbl_dir = YOLO_ROOT / \"labels\" / split\n",
    "    txt_files = sorted(lbl_dir.glob(\"*.txt\"))\n",
    "\n",
    "    pbar = tqdm(txt_files, desc=f\"[{split}] YOLO→COCO\", ncols=90)\n",
    "    for txt in pbar:\n",
    "        stem = txt.stem\n",
    "\n",
    "        # (1) 이미지 검색 (jpg/png/jpeg)\n",
    "        for ext in (\"jpg\", \"png\", \"jpeg\"):\n",
    "            img_path = img_dir / f\"{stem}.{ext}\"\n",
    "            if img_path.exists():\n",
    "                break\n",
    "        else:\n",
    "            pbar.write(f\"❗ 이미지 없음: {stem}.*\")\n",
    "            continue\n",
    "\n",
    "        # (2) 크기 확인\n",
    "        img = cv2.imread(str(img_path))\n",
    "        if img is None:\n",
    "            pbar.write(f\"❗ cv2 로드 실패: {img_path}\")\n",
    "            continue\n",
    "        h, w = img.shape[:2]\n",
    "\n",
    "        # (3) 이미지 링크/복사\n",
    "        out_name = f\"{split}_{stem}{img_path.suffix}\"\n",
    "        out_img  = IMG_OUT_DIR / out_name\n",
    "        link_or_copy(img_path, out_img)\n",
    "\n",
    "        img_id = next_id(cnt, \"img\")\n",
    "        images.append({\n",
    "            \"id\": img_id,\n",
    "            \"file_name\": out_name,\n",
    "            \"width\": w,\n",
    "            \"height\": h,\n",
    "        })\n",
    "\n",
    "        # (4) bbox\n",
    "        for cls, xc, yc, bw, bh in load_yolo_labels(txt):\n",
    "            if cls not in CLASS_NAMES:\n",
    "                pbar.write(f\"❓ 클래스 id {cls} 정의 안 됨\")\n",
    "                continue\n",
    "            bbox = yolo2coco_bbox(xc, yc, bw, bh, w, h)\n",
    "            ann_id = next_id(cnt, \"ann\")\n",
    "            annots.append({\n",
    "                \"id\": ann_id,\n",
    "                \"image_id\": img_id,\n",
    "                \"category_id\": cls + 1,\n",
    "                \"bbox\": bbox,\n",
    "                \"area\": bbox[2] * bbox[3],\n",
    "                \"iscrowd\": 0,\n",
    "            })\n",
    "\n",
    "    # (5) COCO JSON 저장\n",
    "    coco = {\n",
    "        \"info\": {}, \"licenses\": [],\n",
    "        \"categories\": [\n",
    "            {\"id\": i+1, \"name\": n, \"supercategory\": \"\"}\n",
    "            for i, n in CLASS_NAMES.items()\n",
    "        ],\n",
    "        \"images\": images,\n",
    "        \"annotations\": annots,\n",
    "    }\n",
    "    json_out.write_text(json.dumps(coco, indent=2, ensure_ascii=False))\n",
    "    print(f\"✅ {split}: 이미지 {len(images):,}장 / bbox {len(annots):,}개 → {json_out}\")\n",
    "    return cnt[\"img\"], cnt[\"ann\"]\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "[train] YOLO→COCO:  14%|██▉                  | 240831/1742292 [1:24:04<8:43:33, 47.80it/s]"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "❗ 이미지 없음: objects365_v1_00320532.*\n",
      "❗ 이미지 없음: objects365_v1_00320534.*\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "[train] YOLO→COCO:  32%|██████▋              | 552820/1742292 [3:16:04<9:06:08, 36.30it/s]"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "❗ 이미지 없음: objects365_v2_00908726.*\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "[train] YOLO→COCO: 100%|█████████████████████| 1742292/1742292 [13:47:42<00:00, 35.08it/s]\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "✅ train: 이미지 1,742,289장 / bbox 25,407,598개 → /data/ltb/ultralytics/objects365_mmyolo_coco/train_coco.json\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "[val] YOLO→COCO: 100%|██████████████████████████████| 80000/80000 [55:33<00:00, 24.00it/s]\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "✅ val: 이미지 80,000장 / bbox 1,240,587개 → /data/ltb/ultralytics/objects365_mmyolo_coco/val_coco.json\n",
      "\n",
      "=== 변환 완료 ===\n",
      "• 이미지            : /data/ltb/ultralytics/objects365_mmyolo_coco/images\n",
      "• COCO JSON (train) : /data/ltb/ultralytics/objects365_mmyolo_coco/train_coco.json\n",
      "• COCO JSON (val)   : /data/ltb/ultralytics/objects365_mmyolo_coco/val_coco.json\n"
     ]
    }
   ],
   "source": [
    "# ===============================================================\n",
    "# [Cell 4] 실행\n",
    "# ===============================================================\n",
    "last_img, last_ann = convert_split(\"train\", ANNOT_TRAIN)\n",
    "convert_split(\"val\", ANNOT_VAL, last_img, last_ann)\n",
    "\n",
    "print(\"\\n=== 변환 완료 ===\")\n",
    "print(f\"• 이미지            : {IMG_OUT_DIR}\")\n",
    "print(f\"• COCO JSON (train) : {ANNOT_TRAIN}\")\n",
    "print(f\"• COCO JSON (val)   : {ANNOT_VAL}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "🖼️  디버그 샘플 30장 저장 → /data/ltb/ultralytics/objects365_mmyolo_coco/debug_vis\n"
     ]
    }
   ],
   "source": [
    "# ===============================================================\n",
    "# [Cell 5] (옵션) bbox 시각화\n",
    "# ===============================================================\n",
    "if DEBUG_VIS:\n",
    "    import random\n",
    "    from collections import defaultdict\n",
    "    random.seed(RANDOM_SEED)\n",
    "\n",
    "    with open(ANNOT_TRAIN, encoding=\"utf-8\") as f:\n",
    "        coco = json.load(f)\n",
    "\n",
    "    img_meta = {im[\"id\"]: im for im in coco[\"images\"]}\n",
    "    anns_by_img = defaultdict(list)\n",
    "    for a in coco[\"annotations\"]:\n",
    "        anns_by_img[a[\"image_id\"]].append(a)\n",
    "\n",
    "    samples = random.sample(list(img_meta.keys()),\n",
    "                            min(DEBUG_CNT, len(img_meta)))\n",
    "\n",
    "    for img_id in samples:\n",
    "        meta = img_meta[img_id]\n",
    "        img = cv2.imread(str(IMG_OUT_DIR / meta[\"file_name\"]))\n",
    "        if img is None:\n",
    "            continue\n",
    "        for a in anns_by_img[img_id]:\n",
    "            cid = a[\"category_id\"] - 1\n",
    "            clr = ((cid*37)%256, (cid*17)%256, (cid*29)%256)\n",
    "            x,y,w,h = map(int, a[\"bbox\"])\n",
    "            cv2.rectangle(img, (x,y), (x+w, y+h), clr, 2)\n",
    "            cv2.putText(img, CLASS_NAMES[cid][:12],\n",
    "                        (x, max(0, y-5)), cv2.FONT_HERSHEY_SIMPLEX,\n",
    "                        0.5, clr, 1, cv2.LINE_AA)\n",
    "        out_p = DEBUG_OUTDIR / f\"vis_{meta['file_name']}\"\n",
    "        cv2.imwrite(str(out_p), img)\n",
    "    print(f\"🖼️  디버그 샘플 {len(samples)}장 저장 → {DEBUG_OUTDIR}\")\n",
    "else:\n",
    "    print(\"DEBUG_VIS=False (시각화 생략)\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "250520_mmyolo",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.20"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
