{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# =====================================================================\n",
    "# [Cell 1] 초기 설정 – import & 전역 변수\n",
    "# =====================================================================\n",
    "import os, json, random, cv2\n",
    "from pathlib import Path\n",
    "from collections import defaultdict\n",
    "from tqdm.auto import tqdm\n",
    "\n",
    "# ---------------- 사용자 설정 ----------------\n",
    "# (1) 어노테이션 JSON이 들어 있는 루트 디렉터리들\n",
    "ANNOT_DIRS = [\n",
    "    Path(\"/data/ai-hub/fire-dataset/106.화재_발생_예측_데이터(연기_동영상)/01.데이터/1.Training/라벨링데이터(zip)/화재씬\"),\n",
    "    Path(\"/data/ai-hub/fire-dataset/106.화재_발생_예측_데이터(연기_동영상)/01.데이터/1.Training/라벨링데이터(zip)/유사씬\"),\n",
    "    Path(\"/data/ai-hub/fire-dataset/106.화재_발생_예측_데이터(연기_동영상)/01.데이터/1.Training/라벨링데이터(zip)/무관씬\"),\n",
    "]\n",
    "# (2) 이미지(원천 데이터)가 들어 있는 루트 디렉터리들\n",
    "IMG_DIRS = [\n",
    "    Path(\"/data/ai-hub/fire-dataset/106.화재_발생_예측_데이터(연기_동영상)/01.데이터/1.Training/원천데이터(zip)/무관씬\"),\n",
    "    Path(\"data/ai-hub/fire-dataset/106.화재_발생_예측_데이터(연기_동영상)/01.데이터/1.Training/원천데이터(zip)/무관씬_1\"),\n",
    "    Path(\"data/ai-hub/fire-dataset/106.화재_발생_예측_데이터(연기_동영상)/01.데이터/1.Training/원천데이터(zip)/무관씬_2\"),\n",
    "    Path(\"/data/ai-hub/fire-dataset/106.화재_발생_예측_데이터(연기_동영상)/01.데이터/1.Training/원천데이터(zip)/무관씬_3\"),\n",
    "    Path(\"data/ai-hub/fire-dataset/106.화재_발생_예측_데이터(연기_동영상)/01.데이터/1.Training/원천데이터(zip)/무관씬_4\"),\n",
    "    Path(\"data/ai-hub/fire-dataset/106.화재_발생_예측_데이터(연기_동영상)/01.데이터/1.Training/원천데이터(zip)/무관씬_5\"),\n",
    "    Path(\"/data/ai-hub/fire-dataset/106.화재_발생_예측_데이터(연기_동영상)/01.데이터/1.Training/원천데이터(zip)/유사씬_1\"),\n",
    "    Path(\"/data/ai-hub/fire-dataset/106.화재_발생_예측_데이터(연기_동영상)/01.데이터/1.Training/원천데이터(zip)/유사씬_2\"),\n",
    "    Path(\"/data/ai-hub/fire-dataset/106.화재_발생_예측_데이터(연기_동영상)/01.데이터/1.Training/원천데이터(zip)/유사씬_3\"),\n",
    "    Path(\"/data/ai-hub/fire-dataset/106.화재_발생_예측_데이터(연기_동영상)/01.데이터/1.Training/원천데이터(zip)/유사씬_4\"),\n",
    "    Path(\"/data/ai-hub/fire-dataset/106.화재_발생_예측_데이터(연기_동영상)/01.데이터/1.Training/원천데이터(zip)/유사씬_5\"),\n",
    "    Path(\"/data/ai-hub/fire-dataset/106.화재_발생_예측_데이터(연기_동영상)/01.데이터/1.Training/원천데이터(zip)/유사씬_6\"),\n",
    "    Path(\"/data/ai-hub/fire-dataset/106.화재_발생_예측_데이터(연기_동영상)/01.데이터/1.Training/원천데이터(zip)/유사씬_7\"),\n",
    "    Path(\"/data/ai-hub/fire-dataset/106.화재_발생_예측_데이터(연기_동영상)/01.데이터/1.Training/원천데이터(zip)/유사씬_8\"),\n",
    "    Path(\"/data/ai-hub/fire-dataset/106.화재_발생_예측_데이터(연기_동영상)/01.데이터/1.Training/원천데이터(zip)/유사씬_9\"),\n",
    "    Path(\"/data/ai-hub/fire-dataset/106.화재_발생_예측_데이터(연기_동영상)/01.데이터/1.Training/원천데이터(zip)/유사씬_10\"),\n",
    "    Path(\"/data/ai-hub/fire-dataset/106.화재_발생_예측_데이터(연기_동영상)/01.데이터/1.Training/원천데이터(zip)/유사씬_11\"),\n",
    "    Path(\"/data/ai-hub/fire-dataset/106.화재_발생_예측_데이터(연기_동영상)/01.데이터/1.Training/원천데이터(zip)/유사씬_12\"),\n",
    "    Path(\"/data/ai-hub/fire-dataset/106.화재_발생_예측_데이터(연기_동영상)/01.데이터/1.Training/원천데이터(zip)/유사씬_13\"),\n",
    "    Path(\"/data/ai-hub/fire-dataset/106.화재_발생_예측_데이터(연기_동영상)/01.데이터/1.Training/원천데이터(zip)/유사씬_14\"),\n",
    "    Path(\"/data/ai-hub/fire-dataset/106.화재_발생_예측_데이터(연기_동영상)/01.데이터/1.Training/원천데이터(zip)/유사씬_15\"),\n",
    "    Path(\"/data/ai-hub/fire-dataset/106.화재_발생_예측_데이터(연기_동영상)/01.데이터/1.Training/원천데이터(zip)/화재씬\"),\n",
    "    Path(\"/data/ai-hub/fire-dataset/106.화재_발생_예측_데이터(연기_동영상)/01.데이터/1.Training/원천데이터(zip)/화재씬_1\"),\n",
    "    Path(\"/data/ai-hub/fire-dataset/106.화재_발생_예측_데이터(연기_동영상)/01.데이터/1.Training/원천데이터(zip)/화재씬_2\"),\n",
    "    Path(\"/data/ai-hub/fire-dataset/106.화재_발생_예측_데이터(연기_동영상)/01.데이터/1.Training/원천데이터(zip)/화재씬_3\"),\n",
    "    Path(\"/data/ai-hub/fire-dataset/106.화재_발생_예측_데이터(연기_동영상)/01.데이터/1.Training/원천데이터(zip)/화재씬_4\"),\n",
    "    Path(\"/data/ai-hub/fire-dataset/106.화재_발생_예측_데이터(연기_동영상)/01.데이터/1.Training/원천데이터(zip)/화재씬_5\"),\n",
    "    Path(\"/data/ai-hub/fire-dataset/106.화재_발생_예측_데이터(연기_동영상)/01.데이터/1.Training/원천데이터(zip)/화재씬_6\"),\n",
    "    Path(\"/data/ai-hub/fire-dataset/106.화재_발생_예측_데이터(연기_동영상)/01.데이터/1.Training/원천데이터(zip)/화재씬_7\"),\n",
    "    Path(\"/data/ai-hub/fire-dataset/106.화재_발생_예측_데이터(연기_동영상)/01.데이터/1.Training/원천데이터(zip)/화재씬_8\"),\n",
    "    Path(\"/data/ai-hub/fire-dataset/106.화재_발생_예측_데이터(연기_동영상)/01.데이터/1.Training/원천데이터(zip)/화재씬_9\"),\n",
    "    Path(\"/data/ai-hub/fire-dataset/106.화재_발생_예측_데이터(연기_동영상)/01.데이터/1.Training/원천데이터(zip)/화재씬_10\"),\n",
    "    Path(\"/data/ai-hub/fire-dataset/106.화재_발생_예측_데이터(연기_동영상)/01.데이터/1.Training/원천데이터(zip)/화재씬_11\"),\n",
    "    Path(\"/data/ai-hub/fire-dataset/106.화재_발생_예측_데이터(연기_동영상)/01.데이터/1.Training/원천데이터(zip)/화재씬_12\"),\n",
    "    Path(\"/data/ai-hub/fire-dataset/106.화재_발생_예측_데이터(연기_동영상)/01.데이터/1.Training/원천데이터(zip)/화재씬_13\"),\n",
    "]\n",
    "# (3) 클래스 매핑 (데이터 설명서를 참고해 원하는 대로 수정)\n",
    "CLASS_MAPPING = {\n",
    "    \"01\": {\"id\": 1, \"name\": \"black_smoke\"},\n",
    "    \"02\": {\"id\": 2, \"name\": \"gray_smoke\"},\n",
    "    \"03\": {\"id\": 3, \"name\": \"white_smoke\"},\n",
    "    \"04\": {\"id\": 4, \"name\": \"fire\"},\n",
    "    \"05\": {\"id\": 5, \"name\": \"cloud\"},\n",
    "    \"06\": {\"id\": 6, \"name\": \"fog\"},\n",
    "    \"07\": {\"id\": 7, \"name\": \"light\"},\n",
    "    \"08\": {\"id\": 8, \"name\": \"sun_light\"},\n",
    "    \"09\": {\"id\": 9, \"name\": \"not_smoke\"},\n",
    "    \"10\": {\"id\": 10, \"name\": \"grass\"},\n",
    "    \"11\": {\"id\": 11, \"name\": \"background\"},\n",
    "    # 필요하면 01~10까지 모두 매핑 추가\n",
    "}\n",
    "# (4) 출력 COCO JSON 경로\n",
    "OUTPUT_JSON = Path(\"/data/ai-hub/fire-dataset/106.화재_발생_예측_데이터(연기_동영상)/01.데이터/fire_dataset_train.json\")\n",
    "\n",
    "RANDOM_SEED = 42  # 재현성\n",
    "# -------------------------------------------------------------\n",
    "\n",
    "print(\"✅ 설정 완료\")\n",
    "print(f\" • 어노테이션 루트 : {len(ANNOT_DIRS)}개\")\n",
    "print(f\" • 이미지 루트    : {len(IMG_DIRS)}개\")\n",
    "print(f\" • 출력 JSON      : {OUTPUT_JSON}\")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# =====================================================================\n",
    "# [Cell 2] 이미지 인덱싱 – 이미지 파일 → 절대경로 dict 생성\n",
    "#          (1TB라도 딕셔너리 한 번 만들어두면 빠르게 매칭 가능)\n",
    "# =====================================================================\n",
    "from glob import glob\n",
    "\n",
    "def build_image_index(img_dirs):\n",
    "    \"\"\"\n",
    "    주어진 여러 루트 디렉터리를 모두 순회하며\n",
    "    {파일명: 절대경로} 형태로 매핑을 만든다.\n",
    "    파일명이 중복되면 가장 먼저 찾은 경로를 사용.\n",
    "    \"\"\"\n",
    "    idx = dict()\n",
    "    for root in img_dirs:\n",
    "        print(f\"📂 이미지 탐색 시작 → {root}\")\n",
    "        for ext in (\"*.jpg\", \"*.jpeg\", \"*.png\"):          # 필요한 확장자 추가\n",
    "            pattern = str(root / \"**\" / ext)\n",
    "            for fpath in glob(pattern, recursive=True):\n",
    "                fname = Path(fpath).name\n",
    "                if fname not in idx:                      # 중복 방지\n",
    "                    idx[fname] = fpath\n",
    "        print(f\"   ↳ 누적 {len(idx):,}개 파일 인덱싱\")\n",
    "    print(f\"✅ 인덱싱 완료 – 총 {len(idx):,}개 이미지\")\n",
    "    return idx\n",
    "\n",
    "IMG_INDEX = build_image_index(IMG_DIRS)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# %% ==================================================================\n",
    "# [Cell 3] 유틸리티 함수 – polygon → bbox, COCO 구조 helper  ★BOM 대응\n",
    "# =====================================================================\n",
    "import numpy as np\n",
    "\n",
    "def polygon_to_bbox(poly):\n",
    "    \"\"\"\n",
    "    polygon(list[list[x,y]]) → [x_min, y_min, w, h] (절대좌표 float)\n",
    "    \"\"\"\n",
    "    pts = np.array(poly, dtype=np.float32)\n",
    "    x_min, y_min = pts.min(axis=0)\n",
    "    x_max, y_max = pts.max(axis=0)\n",
    "    return [float(x_min), float(y_min), float(x_max - x_min), float(y_max - y_min)]\n",
    "\n",
    "def safe_json_load(path: Path):\n",
    "    \"\"\"\n",
    "    UTF-8 - BOM 이 섞여 있어도 안전하게 JSON 로드\n",
    "    \"\"\"\n",
    "    try:\n",
    "        with open(path, \"r\", encoding=\"utf-8-sig\") as f:\n",
    "            return json.load(f)\n",
    "    except json.JSONDecodeError as e:\n",
    "        # 다른 형식 오류가 있으면 None 반환\n",
    "        print(f\"    ↳ JSONDecodeError: {path.name} → {e}\")\n",
    "        return None\n",
    "\n",
    "def load_fire_json(json_path: Path):\n",
    "    \"\"\"\n",
    "    화재 데이터셋 JSON 1개를 읽고, image 메타 + annots 리스트 반환\n",
    "    실패하면 (None, None) 반환 → 상위 루프에서 건너뜀\n",
    "    \"\"\"\n",
    "    data = safe_json_load(json_path)\n",
    "    if data is None:\n",
    "        return None, None\n",
    "\n",
    "    # ---------------- 이미지 메타 ----------------\n",
    "    img_info = data[\"image\"]\n",
    "    filename = img_info[\"filename\"]\n",
    "    width, height = img_info[\"resolution\"]   # [w, h]\n",
    "\n",
    "    # ---------------- 어노테이션 -----------------\n",
    "    records = []\n",
    "    for ann in data.get(\"annotations\", []):\n",
    "        cls_code = ann.get(\"class\")\n",
    "        if cls_code not in CLASS_MAPPING:\n",
    "            continue\n",
    "\n",
    "        # polygon 또는 bbox 둘 중 하나만 있으면 OK\n",
    "        if ann.get(\"polygon\"):\n",
    "            bbox = polygon_to_bbox(ann[\"polygon\"])\n",
    "        elif ann.get(\"bbox\"):\n",
    "            bb = ann[\"bbox\"]                 # [xmin,ymin,xmax,ymax]\n",
    "            if len(bb) != 4:\n",
    "                continue\n",
    "            bbox = [bb[0], bb[1], bb[2]-bb[0], bb[3]-bb[1]]\n",
    "        else:\n",
    "            continue\n",
    "\n",
    "        records.append({\n",
    "            \"class_code\": cls_code,\n",
    "            \"bbox\": bbox\n",
    "        })\n",
    "\n",
    "    return {\"filename\": filename,\n",
    "            \"width\": width,\n",
    "            \"height\": height}, records\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# %% ==================================================================\n",
    "# [Cell 4] 메인 변환 루프 – 모든 JSON → COCO dict 작성  ★예외 처리 강화\n",
    "# =====================================================================\n",
    "def convert_to_coco(annot_dirs, img_index):\n",
    "    images, annotations = [], []\n",
    "    img_id_ctr, ann_id_ctr = 0, 0\n",
    "\n",
    "    random.seed(RANDOM_SEED)\n",
    "    json_files = []\n",
    "    for adir in annot_dirs:\n",
    "        json_files.extend(sorted(adir.glob(\"*.json\")))\n",
    "    random.shuffle(json_files)\n",
    "\n",
    "    pbar = tqdm(json_files, desc=\"JSON 변환\", ncols=110)\n",
    "    for js_path in pbar:\n",
    "        img_meta, anns = load_fire_json(js_path)\n",
    "        if img_meta is None:          # 파싱 실패\n",
    "            continue\n",
    "\n",
    "        fname = img_meta[\"filename\"]\n",
    "        if fname not in img_index:\n",
    "            pbar.write(f\"⚠️ 이미지 파일 없음: {fname}\")\n",
    "            continue\n",
    "\n",
    "        # ---------- 이미지 COCO 엔트리 ----------\n",
    "        img_id_ctr += 1\n",
    "        images.append({\n",
    "            \"id\"       : img_id_ctr,\n",
    "            \"file_name\": img_index[fname],   # 절대경로\n",
    "            \"width\"    : img_meta[\"width\"],\n",
    "            \"height\"   : img_meta[\"height\"],\n",
    "        })\n",
    "\n",
    "        # ---------- bbox COCO 엔트리 ----------\n",
    "        for ann in anns:\n",
    "            ann_id_ctr += 1\n",
    "            cid  = CLASS_MAPPING[ann[\"class_code\"]][\"id\"]\n",
    "            bbox = ann[\"bbox\"]\n",
    "            annotations.append({\n",
    "                \"id\"         : ann_id_ctr,\n",
    "                \"image_id\"   : img_id_ctr,\n",
    "                \"category_id\": cid,\n",
    "                \"bbox\"       : bbox,\n",
    "                \"area\"       : bbox[2] * bbox[3],\n",
    "                \"iscrowd\"    : 0,\n",
    "            })\n",
    "\n",
    "    coco_dict = {\n",
    "        \"info\"       : {\"description\": \"AI Hub Fire Prediction Dataset – converted\"},\n",
    "        \"licenses\"   : [],\n",
    "        \"categories\" : list(CLASS_MAPPING.values()),\n",
    "        \"images\"     : images,\n",
    "        \"annotations\": annotations,\n",
    "    }\n",
    "    print(f\"✅ 변환 요약 → 이미지 {len(images):,}장, bbox {len(annotations):,}개\")\n",
    "    return coco_dict\n",
    "\n",
    "COCO_DATA = convert_to_coco(ANNOT_DIRS, IMG_INDEX)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# =====================================================================\n",
    "# [Cell 5] COCO JSON 저장\n",
    "# =====================================================================\n",
    "OUTPUT_JSON.parent.mkdir(parents=True, exist_ok=True)\n",
    "with OUTPUT_JSON.open(\"w\", encoding=\"utf-8\") as fp:\n",
    "    json.dump(COCO_DATA, fp, ensure_ascii=False, indent=2)\n",
    "\n",
    "print(f\"✅ COCO JSON 저장 완료 → {OUTPUT_JSON}\")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# =====================================================================\n",
    "# [Cell 6] (선택) 간단 디버그 시각화 – 샘플 5장\n",
    "# =====================================================================\n",
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "\n",
    "DEBUG_VIS_NUM = 5\n",
    "sample_imgs = random.sample(COCO_DATA[\"images\"], min(DEBUG_VIS_NUM, len(COCO_DATA[\"images\"])))\n",
    "ann_by_img = defaultdict(list)\n",
    "for a in COCO_DATA[\"annotations\"]:\n",
    "    ann_by_img[a[\"image_id\"]].append(a)\n",
    "\n",
    "for img_meta in sample_imgs:\n",
    "    img = cv2.imread(img_meta[\"file_name\"])\n",
    "    if img is None:\n",
    "        print(f\"❗ 이미지 로드 실패: {img_meta['file_name']}\")\n",
    "        continue\n",
    "    for ann in ann_by_img[img_meta[\"id\"]]:\n",
    "        cid = ann[\"category_id\"]\n",
    "        clr = ((cid*37)%256, (cid*17)%256, (cid*13)%256)\n",
    "        x,y,w,h = map(int, ann[\"bbox\"])\n",
    "        cv2.rectangle(img,(x,y),(x+w,y+h),clr,2)\n",
    "        cv2.putText(img, str(cid), (x, y-5),\n",
    "                    cv2.FONT_HERSHEY_SIMPLEX, 0.6, clr, 2)\n",
    "\n",
    "    plt.figure(figsize=(6,6)); plt.axis(\"off\")\n",
    "    plt.title(Path(img_meta[\"file_name\"]).name)\n",
    "    plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))\n",
    "    plt.show()\n"
   ]
  }
 ],
 "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
}
