{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# %% [markdown]\n",
    "# # [1] 초기 설정 & 라이브러리 임포트\n",
    "\n",
    "# %%\n",
    "import os\n",
    "import sys\n",
    "import cv2\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "import torch\n",
    "\n",
    "from mmengine.registry import init_default_scope\n",
    "from mmdet.apis import init_detector, inference_detector\n",
    "from mmdet.utils import register_all_modules\n",
    "\n",
    "# 필요 시 mmyolo path 추가\n",
    "mmyolo_dir = \"/ltb/media/ltb/90887173887158A4/Users/ltb/회사용/4090_옮길거/mmyolo\"\n",
    "if mmyolo_dir not in sys.path:\n",
    "    sys.path.append(mmyolo_dir)\n",
    "\n",
    "# 레지스트리 등록\n",
    "register_all_modules()\n",
    "init_default_scope('mmdet')\n",
    "\n",
    "# ----------------------------------------------------\n",
    "# (!!!) 사용자 지정 파라미터\n",
    "# ----------------------------------------------------\n",
    "VIDEO_PATH = \"/ltb/media/ltb/90887173887158A4/Users/ltb/회사용/4090_옮길거/val_sample/포스코_광주_고양_합침/KakaoTalk_20250108_154050505.mp4\"\n",
    "MAIN_OUTPUT_DIR = \"/ltb/media/ltb/90887173887158A4/Users/ltb/회사용/4090_옮길거/val_sample/모델비교_아웃풋들\"\n",
    "device = \"cuda:0\" if torch.cuda.is_available() else \"cpu\"\n",
    "print(f\"🚀 디바이스: {device}\")\n",
    "\n",
    "# 4개 모델 정보\n",
    "MODEL_INFOS = [\n",
    "    {\n",
    "        \"model_name\": \"1819crc\",\n",
    "        \"config\": \"/ltb/media/ltb/90887173887158A4/Users/ltb/회사용/4090_옮길거/mmyolo/work_dirs/ff_base_1819+cut+rotate+cutrota_1,1,3_01/yolo8_large_960size_ff_base_1819+cut+rotate+cutrota_1,1,3.py\",\n",
    "        \"checkpoint\": \"/ltb/media/ltb/90887173887158A4/Users/ltb/회사용/4090_옮길거/mmyolo/work_dirs/ff_base_1819+cut+rotate+cutrota_1,1,3_01/best_coco_bbox_mAP_epoch_6.pth\",\n",
    "        \"class_names\": [\"worker\", \"helmet\", \"head\"],\n",
    "        \"score_thresholds\": {\"worker\":0.1, \"helmet\":0.1, \"head\":0.1}\n",
    "    },\n",
    "    {\n",
    "        \"model_name\": \"resize_1819crc\",\n",
    "        \"config\": \"/ltb/media/ltb/90887173887158A4/Users/ltb/회사용/4090_옮길거/mmyolo/work_dirs/ff_base_resize_1,2,0.5_1819+cut+rotate+cutrota_1,1,3_01/yolo8_large_960size_ff_base_resize_1,2,0.5_1819+cut+rotate+cutrota_1,1,3.py\",\n",
    "        \"checkpoint\": \"/ltb/media/ltb/90887173887158A4/Users/ltb/회사용/4090_옮길거/mmyolo/work_dirs/ff_base_resize_1,2,0.5_1819+cut+rotate+cutrota_1,1,3_01/best_coco_bbox_mAP_epoch_8.pth\",\n",
    "        \"class_names\": [\"worker\", \"helmet\", \"head\"],\n",
    "        \"score_thresholds\": {\"worker\":0.1, \"helmet\":0.1, \"head\":0.1}\n",
    "    },\n",
    "    {\n",
    "        \"model_name\": \"load_1819\",\n",
    "        \"config\": \"/ltb/media/ltb/90887173887158A4/Users/ltb/회사용/4090_옮길거/mmyolo/work_dirs/it1_load_epoch16_1819_add_head_2,1,10_01/yolov8_l_960_custom_add_head_ff_result_2,1,10.py\",\n",
    "        \"checkpoint\": \"/ltb/media/ltb/90887173887158A4/Users/ltb/회사용/4090_옮길거/mmyolo/work_dirs/it1_load_epoch16_1819_add_head_2,1,10_01/best_coco_bbox_mAP_epoch_19.pth\",\n",
    "        \"class_names\": [\"worker\", \"helmet\", \"head\"],\n",
    "        \"score_thresholds\": {\"worker\":0.1, \"helmet\":0.1, \"head\":0.1}\n",
    "    },\n",
    "    {\n",
    "        \"model_name\": \"fall_960\",\n",
    "        \"config\": \"/ltb/media/ltb/90887173887158A4/Users/ltb/회사용/4090_옮길거/mmyolo/work_dirs/it1_roboflow_fall_only_rotate_960_yolo_01/yolov8_l_960_custom_fall.py\",\n",
    "        \"checkpoint\": \"/ltb/media/ltb/90887173887158A4/Users/ltb/회사용/4090_옮길거/mmyolo/work_dirs/it1_roboflow_fall_only_rotate_960_yolo_01/best_coco_bbox_mAP_epoch_15.pth\",\n",
    "        \"class_names\": [\"worker\"],  # fall 모델은 1클래스만\n",
    "        \"score_thresholds\": {\"worker\":0.1}\n",
    "    },\n",
    "]\n",
    "\n",
    "# 클래스별 색상 매핑 (필요시 수정)\n",
    "CLASS_COLORS = {\n",
    "    \"worker\":  (0, 255, 0),   # 초록\n",
    "    \"helmet\":  (255, 0, 0),   # 파랑\n",
    "    \"head\":    (0, 0, 255),   # 빨강\n",
    "    # 그 외 클래스는 기본 회색\n",
    "}\n",
    "\n",
    "print(\"✅ [1] 초기 설정 완료.\")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# %% [markdown]\n",
    "# # [2] 결과 폴더 생성 함수\n",
    "# \n",
    "# - exp, exp2... 식으로 저장 폴더 생성해, 중복 방지.\n",
    "\n",
    "# %%\n",
    "def get_new_output_folder(base_dir, prefix=\"exp\"):\n",
    "    i = 1\n",
    "    while True:\n",
    "        folder_name = prefix if i == 1 else f\"{prefix}{i}\"\n",
    "        new_dir = os.path.join(base_dir, folder_name)\n",
    "        if not os.path.exists(new_dir):\n",
    "            os.makedirs(new_dir)\n",
    "            return new_dir\n",
    "        i += 1\n",
    "\n",
    "output_folder = get_new_output_folder(MAIN_OUTPUT_DIR, prefix=\"exp\")\n",
    "print(f\"✅ 결과 저장 폴더: {output_folder}\")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# %% [markdown]\n",
    "# # [3] 모델 로드 & 디텍션 함수\n",
    "# \n",
    "# - init_detector()\n",
    "# - detect_objects() -> \"worker\"만 남긴다\n",
    "# - draw_detections_advanced() -> bbox 옆에 score, w,h,area 표시\n",
    "\n",
    "# %%\n",
    "def load_model(cfg, ckp, device=\"cpu\"):\n",
    "    model = init_detector(cfg, ckp, device=device)\n",
    "    return model\n",
    "\n",
    "def detect_objects(frame_bgr, model, class_names, class_score_thresholds):\n",
    "    \"\"\"\n",
    "    -> \"worker\" 클래스만 유지. helmet/head는 무시.\n",
    "    \"\"\"\n",
    "    result = inference_detector(model, frame_bgr)\n",
    "    if not hasattr(result, \"pred_instances\"):\n",
    "        return []\n",
    "    \n",
    "    bboxes = result.pred_instances.bboxes\n",
    "    scores = result.pred_instances.scores\n",
    "    labels = result.pred_instances.labels\n",
    "    \n",
    "    out = []\n",
    "    for i in range(len(bboxes)):\n",
    "        box = bboxes[i].tolist()  # [x1,y1,x2,y2]\n",
    "        sc = scores[i].item()\n",
    "        cid = labels[i].item()\n",
    "        # class 이름\n",
    "        if cid < len(class_names):\n",
    "            cname = class_names[cid]\n",
    "        else:\n",
    "            cname = f\"cls_{cid}\"\n",
    "        \n",
    "        # 여기서 \"worker\"만 필터\n",
    "        if cname != \"worker\":\n",
    "            continue\n",
    "        \n",
    "        thr = class_score_thresholds.get(\"worker\", 0.1)\n",
    "        if sc < thr:\n",
    "            continue\n",
    "        \n",
    "        x1, y1, x2, y2 = box\n",
    "        w = x2 - x1\n",
    "        h = y2 - y1\n",
    "        area = w*h\n",
    "        out.append({\n",
    "            \"bbox\":[x1,y1,x2,y2],\n",
    "            \"score\": sc,\n",
    "            \"class_name\": \"worker\",  # 고정\n",
    "            \"width\": w,\n",
    "            \"height\":h,\n",
    "            \"area\": area\n",
    "        })\n",
    "    return out\n",
    "\n",
    "def draw_detections_advanced(frame_bgr, detections, class_colors, font_scale=1.0, font_thick=2):\n",
    "    \"\"\"\n",
    "    조금 더 크게 표시. (font_scale=1.0)\n",
    "    \"\"\"\n",
    "    result = frame_bgr.copy()\n",
    "    for det in detections:\n",
    "        x1,y1,x2,y2 = map(int, det[\"bbox\"])\n",
    "        sc = det[\"score\"]\n",
    "        w  = det[\"width\"]\n",
    "        h  = det[\"height\"]\n",
    "        area = det[\"area\"]\n",
    "        cname = det[\"class_name\"]\n",
    "        \n",
    "        color = class_colors.get(cname, (128,128,128))\n",
    "        \n",
    "        # bbox\n",
    "        cv2.rectangle(result, (x1,y1), (x2,y2), color, 3)\n",
    "        # 텍스트\n",
    "        line1 = f\"{cname} {sc:.2f}\"\n",
    "        line2 = f\"W={w:.0f},H={h:.0f},A={area:.0f}\"\n",
    "        \n",
    "        # 위치 계산\n",
    "        y_text = y1-10\n",
    "        cv2.putText(result, line1, (x1, y_text),\n",
    "                    cv2.FONT_HERSHEY_SIMPLEX, font_scale, color, font_thick, cv2.LINE_AA)\n",
    "        y_text -= 35\n",
    "        cv2.putText(result, line2, (x1, max(y_text,10)),\n",
    "                    cv2.FONT_HERSHEY_SIMPLEX, font_scale, color, font_thick, cv2.LINE_AA)\n",
    "        \n",
    "    return result\n",
    "\n",
    "print(\"✅ [3] 모델 로드 & 디텍션 함수 준비 완료.\")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# %% [markdown]\n",
    "# # [4] 부분 프레임 추론 & 2x2 합치기\n",
    "\n",
    "# %%\n",
    "def make_image_grid(img_list, rows=2, cols=2):\n",
    "    \"\"\"\n",
    "    2x2로 합친 뒤 반환. \n",
    "    img_list: 최대 4장. (4개 이하라면 blank로 채움)\n",
    "    \"\"\"\n",
    "    if not img_list:\n",
    "        return None\n",
    "    # 만약 4개보다 적으면 blank 채우기\n",
    "    base = img_list[0]\n",
    "    h, w = base.shape[:2]\n",
    "    blank = np.zeros_like(base)\n",
    "    \n",
    "    while len(img_list) < rows*cols:\n",
    "        img_list.append(blank)\n",
    "    \n",
    "    grid = np.zeros((h*rows, w*cols, 3), dtype=np.uint8)\n",
    "    idx=0\n",
    "    for r in range(rows):\n",
    "        for c in range(cols):\n",
    "            grid[r*h:(r+1)*h, c*w:(c+1)*w] = img_list[idx]\n",
    "            idx+=1\n",
    "    return grid\n",
    "\n",
    "def run_detection_for_time(video_path, time_in_seconds, loaded_models,\n",
    "                           frame_range=2, \n",
    "                           save_dir=None):\n",
    "    \"\"\"\n",
    "    - time_in_seconds -> frame_index\n",
    "    - frame_index ± frame_range 추론\n",
    "    - 'worker'만 디텍션 \n",
    "    - 4개 모델 결과를 2x2로 합친 뒤, 해상도 2배로 resize하여 저장\n",
    "    - 리턴: det_records\n",
    "    \"\"\"\n",
    "    det_records = []\n",
    "    \n",
    "    cap = cv2.VideoCapture(video_path)\n",
    "    if not cap.isOpened():\n",
    "        print(f\"[에러] 영상 열기 실패: {video_path}\")\n",
    "        return det_records\n",
    "    \n",
    "    fps = cap.get(cv2.CAP_PROP_FPS)\n",
    "    total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))\n",
    "    \n",
    "    targ_f = int(time_in_seconds * fps)\n",
    "    start_f = max(0, targ_f - frame_range)\n",
    "    end_f   = min(total_frames-1, targ_f + frame_range)\n",
    "    \n",
    "    print(f\"🎯 초={time_in_seconds}, frame_range=±{frame_range}\")\n",
    "    print(f\"   -> 프레임 구간: [{start_f} ~ {end_f}]\")\n",
    "    \n",
    "    for fidx in range(start_f, end_f+1):\n",
    "        cap.set(cv2.CAP_PROP_POS_FRAMES, fidx)\n",
    "        ret, frame_bgr = cap.read()\n",
    "        if not ret:\n",
    "            print(f\"[경고] 프레임 {fidx} 읽기 실패.\")\n",
    "            continue\n",
    "        \n",
    "        model_vis_list = []\n",
    "        model_summaries = []\n",
    "        \n",
    "        for m_info in loaded_models:\n",
    "            m_name = m_info[\"model_name\"]\n",
    "            m_obj  = m_info[\"model_obj\"]\n",
    "            cls_n  = m_info[\"class_names\"]\n",
    "            thr_d  = m_info[\"score_thresholds\"]\n",
    "            \n",
    "            # detect\n",
    "            dets = detect_objects(frame_bgr, m_obj, cls_n, thr_d)  # worker만\n",
    "            # draw\n",
    "            vis = draw_detections_advanced(frame_bgr, dets, CLASS_COLORS,\n",
    "                                           font_scale=1.0, font_thick=2)\n",
    "            \n",
    "            model_vis_list.append(vis)\n",
    "            \n",
    "            # record\n",
    "            for d in dets:\n",
    "                rec = {\n",
    "                    \"model_name\": m_name,\n",
    "                    \"frame_idx\": fidx,\n",
    "                    \"score\": d[\"score\"],\n",
    "                    \"width\": d[\"width\"],\n",
    "                    \"height\": d[\"height\"],\n",
    "                    \"area\": d[\"area\"]\n",
    "                }\n",
    "                det_records.append(rec)\n",
    "            \n",
    "            # 요약\n",
    "            n_det = len(dets)\n",
    "            mean_sc = np.mean([d[\"score\"] for d in dets]) if n_det>0 else 0\n",
    "            mean_ar = np.mean([d[\"area\"]  for d in dets]) if n_det>0 else 0\n",
    "            model_summaries.append((m_name, n_det, mean_sc, mean_ar))\n",
    "        \n",
    "        if model_vis_list:\n",
    "            grid_img = make_image_grid(model_vis_list,2,2)\n",
    "            \n",
    "            # 해상도 2배로 키우기\n",
    "            gh, gw = grid_img.shape[:2]\n",
    "            # 배율. 여기서는 1.5배 or 2배 (자유롭게)\n",
    "            scale_factor = 2.0\n",
    "            grid_resized = cv2.resize(grid_img, (int(gw*scale_factor), int(gh*scale_factor)))\n",
    "            \n",
    "            # 상단 요약 텍스트\n",
    "            summ_text = f\"Frame {fidx}\\n\"\n",
    "            for (m_name, n_det, m_sc, m_ar) in model_summaries:\n",
    "                summ_text += f\"{m_name}: #Wkr={n_det}, avgSc={m_sc:.2f}, avgArea={m_ar:.1f}\\n\"\n",
    "            \n",
    "            # 텍스트 표시 (왼쪽 위에)\n",
    "            y_off = 40\n",
    "            for line in summ_text.split(\"\\n\"):\n",
    "                cv2.putText(grid_resized, line, (20,y_off),\n",
    "                            cv2.FONT_HERSHEY_SIMPLEX, 1.2, (255,255,255), 3, cv2.LINE_AA)\n",
    "                y_off += 40\n",
    "            \n",
    "            if save_dir:\n",
    "                base_nm = os.path.splitext(os.path.basename(video_path))[0]\n",
    "                out_fn  = f\"{base_nm}_frame{fidx:04d}_4model_grid.jpg\"\n",
    "                out_p   = os.path.join(save_dir, out_fn)\n",
    "                \n",
    "                cv2.imwrite(out_p, grid_resized)\n",
    "                plt.figure(figsize=(12,10))\n",
    "                plt.imshow(cv2.cvtColor(grid_resized, cv2.COLOR_BGR2RGB))\n",
    "                plt.title(f\"Frame {fidx} - Worker Only\")\n",
    "                plt.axis(\"off\")\n",
    "                plt.show()\n",
    "                \n",
    "    cap.release()\n",
    "    return det_records\n",
    "\n",
    "print(\"✅ [4] 부분 프레임 추론 & 합치기 함수 완료.\")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# %% [markdown]\n",
    "# # [5] 실행 예시\n",
    "# \n",
    "# - 모델들 로드\n",
    "# - 특정 초(예: 10초) ± 2프레임\n",
    "# - 결과 이미지 + df_det\n",
    "\n",
    "# %%\n",
    "loaded_models = []\n",
    "for info in MODEL_INFOS:\n",
    "    print(f\"🔄 로딩: {info['model_name']}\")\n",
    "    mdl = load_model(info[\"config\"], info[\"checkpoint\"], device=device)\n",
    "    loaded_models.append({\n",
    "        \"model_name\": info[\"model_name\"],\n",
    "        \"model_obj\": mdl,\n",
    "        \"class_names\": info[\"class_names\"],\n",
    "        \"score_thresholds\": info[\"score_thresholds\"]\n",
    "    })\n",
    "\n",
    "time_in_sec = 26.0\n",
    "frame_range = 1\n",
    "\n",
    "det_list = run_detection_for_time(VIDEO_PATH, time_in_sec, loaded_models,\n",
    "                                  frame_range=frame_range,\n",
    "                                  save_dir=output_folder)\n",
    "df_det = pd.DataFrame(det_list)\n",
    "print(\"df_det.shape:\", df_det.shape)\n",
    "print(df_det.head(10))\n",
    "\n",
    "print(\"✅ [5] 실행 예시 완료.\")\n",
    "print(f\"결과 폴더: {output_folder}\")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# %% [markdown]\n",
    "# # [6] 프레임별 모델별 분석 & 그래프\n",
    "# \n",
    "# - df_det: columns=[model_name, frame_idx, score, width, height, area]\n",
    "# - 1) groupby([\"frame_idx\",\"model_name\"]) -> 평균(width,height,area,score), count(*) => df_frame_model\n",
    "# - 2) 그 df를 이용해 (가로, 세로, area, score, count) 각 지표별 그래프\n",
    "# - 3) 한꺼번에 그리는 버전(서브플롯)도 저장\n",
    "\n",
    "# %%\n",
    "if df_det.empty:\n",
    "    print(\"⚠️ df_det가 비어있습니다. worker 검출이 없었던 듯합니다.\")\n",
    "else:\n",
    "    # 1) groupby\n",
    "    df_frame_model = df_det.groupby([\"frame_idx\",\"model_name\"]).agg({\n",
    "        \"width\":\"mean\",\n",
    "        \"height\":\"mean\",\n",
    "        \"area\":\"mean\",\n",
    "        \"score\":\"mean\",\n",
    "    }).rename(columns={\n",
    "        \"width\":\"avg_width\",\n",
    "        \"height\":\"avg_height\",\n",
    "        \"area\":\"avg_area\",\n",
    "        \"score\":\"avg_score\"\n",
    "    }).reset_index()\n",
    "    \n",
    "    # count\n",
    "    df_count = df_det.groupby([\"frame_idx\",\"model_name\"]).size().reset_index(name=\"count\")\n",
    "    df_frame_model = pd.merge(df_frame_model, df_count, on=[\"frame_idx\",\"model_name\"], how=\"left\")\n",
    "    \n",
    "    print(\"=== df_frame_model (프레임 x 모델) ===\")\n",
    "    print(df_frame_model.head(10))\n",
    "    \n",
    "    # 2) 지표별 그래프: frame_idx x-axis\n",
    "    # 모델별 다른 색깔의 라인\n",
    "    metrics = [\"avg_width\",\"avg_height\",\"avg_area\",\"avg_score\",\"count\"]\n",
    "    \n",
    "    # (a) 지표별 그래프 각각 저장\n",
    "    for metric in metrics:\n",
    "        plt.figure(figsize=(8,6))\n",
    "        for m_name in df_frame_model[\"model_name\"].unique():\n",
    "            sub_df = df_frame_model[df_frame_model[\"model_name\"]==m_name].sort_values(\"frame_idx\")\n",
    "            plt.plot(sub_df[\"frame_idx\"], sub_df[metric], marker='o', label=m_name)\n",
    "        \n",
    "        plt.title(f\"{metric} vs frame_idx\")\n",
    "        plt.xlabel(\"Frame_idx\")\n",
    "        plt.ylabel(metric)\n",
    "        plt.legend()\n",
    "        out_fn = os.path.join(output_folder, f\"graph_{metric}_vs_frame.png\")\n",
    "        plt.savefig(out_fn, dpi=150)\n",
    "        plt.show()\n",
    "    \n",
    "    # (b) 한꺼번에 (서브플롯 5개) => 가독성이 떨어질 수 있어도 참고용\n",
    "    fig, axes = plt.subplots(3,2, figsize=(12,12))  # 3행2열 => 6칸중 5칸 사용\n",
    "    ax_list = axes.ravel()\n",
    "    \n",
    "    for i, metric in enumerate(metrics):\n",
    "        ax = ax_list[i]\n",
    "        for m_name in df_frame_model[\"model_name\"].unique():\n",
    "            sub_df = df_frame_model[df_frame_model[\"model_name\"]==m_name].sort_values(\"frame_idx\")\n",
    "            ax.plot(sub_df[\"frame_idx\"], sub_df[metric], marker='o', label=m_name)\n",
    "        ax.set_title(f\"{metric} vs Frame\")\n",
    "        ax.set_xlabel(\"Frame\")\n",
    "        ax.set_ylabel(metric)\n",
    "        ax.legend()\n",
    "    \n",
    "    plt.tight_layout()\n",
    "    sp_fn = os.path.join(output_folder, \"all_metrics_subplots.png\")\n",
    "    plt.savefig(sp_fn, dpi=150)\n",
    "    plt.show()\n",
    "    print(f\"✅ 서브플롯 그래프 저장: {sp_fn}\")\n",
    "    \n",
    "    # CSV 저장\n",
    "    csv_path = os.path.join(output_folder, \"frame_model_stats.csv\")\n",
    "    df_frame_model.to_csv(csv_path, index=False)\n",
    "    print(f\"✅ 프레임별 모델별 통계 CSV 저장: {csv_path}\")\n",
    "    \n",
    "print(\"🎉 [6] 그래프 & 표 작성 완료.\")\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "ltb",
   "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
}
