{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "시각화 폰트 관련\n",
    "\n",
    "폰트 크기를 pt(또는 cv2 기준)로 바꾸기보다는, 사용 예시와 추가 주석을 통해 폰트 크기의 의미를 명확히 안내합니다.\n",
    "\n",
    "폰트 라인 굵기 + BBox 라인 굵기 각각 조절 가능하도록 전역 파라미터화.\n",
    "\n",
    "폰트 종류 지정\n",
    "\n",
    "어떤 폰트가 주로 사용되는지 예시 주석 추가: cv2.FONT_HERSHEY_SIMPLEX, cv2.FONT_HERSHEY_DUPLEX, etc.\n",
    "\n",
    "한글 폰트(예: Pretendard, 나눔고딕) 사용 시, OpenCV에서 직접 TTF 경로를 지정하는 건 조금 까다롭지만, [예시] 섹션에 “freetype” 모듈이나 PIL을 이용해 그리는 방법 등을 언급하는 예시를 주석으로 제시합니다.\n",
    "\n",
    "하이퍼 파라미터 설명 보강\n",
    "\n",
    "[Cell 1]에서 수치와 기호(예: ‘0.3’는 30% 통과 비율) 등을 비유와 함께 주석으로 달았습니다.\n",
    "\n",
    "다각형 구역 지정 + 특정 클래스 감지 시 상단 경고 표시\n",
    "\n",
    "on/off로 제어 가능 (ENABLE_REGION_ALARM = True/False).\n",
    "\n",
    "다각형은 (A) 직접 좌표로 입력, (B) 노트북에서 마우스 클릭으로 ROI 지정(여기서는 GUI 없는 환경이라 시뮬레이션 식으로 예시), 두 방식을 모두 주석.\n",
    "\n",
    "클래스가 구역 안에 들어있으면, 상단에 경고 텍스트 표시(텍스트 내용, 폰트, 색상, 위치 등 전역 파라미터로 조절).\n",
    "\n",
    "중간에 1개의 프레임을 샘플링하여 구역 안의 detection을 확인/디버깅하는 코드를 추가.\n",
    "\n",
    "셀 번호를 늘리고, 각 셀의 목적을 주석으로 자세히 적음\n",
    "\n",
    "마지막에 한 번 더 “전체 코드”를 면밀히 검토하여 변수 충돌이 없는지 확인.\n",
    "\n",
    "아래 전체 코드를 복사·붙여넣기하여 Jupyter Notebook(.ipynb)으로 사용하면 됩니다.\n",
    "(또는 .py로 저장해도 되지만, Notebook형식의 Markdown/주석이 좀 더 풍부합니다.)\n",
    "\n",
    "목차\n",
    "\n",
    "[Cell 1] 전역 설정(하이퍼파라미터 + 폰트설정 + 알람영역 on/off 등)\n",
    "\n",
    "[Cell 2] 기본 헬퍼 함수 (IoU, distance, color 등)\n",
    "\n",
    "[Cell 3] 모델 로드\n",
    "\n",
    "[Cell 4] 디텍션 & 시각화 함수 (폰트/라인 굵기, 폰트 종류)\n",
    "\n",
    "[Cell 5] Polygon ROI 설정 + 디버깅 (중간 디버그 프레임 저장)\n",
    "\n",
    "[Cell 6] ByteTracker + 슬라이딩 윈도우\n",
    "\n",
    "[Cell 7] 앙상블 로직 (whhb_worker+fall_worker, helmet+head)\n",
    "\n",
    "[Cell 8] 메인 처리 함수\n",
    "\n",
    "[Cell 9] 실행부(main)\n",
    "\n",
    "[Cell 10] 최종 확인"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "상세 설명 / 체크리스트\n",
    "폰트 크기\n",
    "\n",
    "OpenCV의 fontScale는 pt 단위가 아닌, 픽셀 변환 비례 값입니다. 즉,\n",
    "\n",
    "fontScale=1: 기본 크기\n",
    "\n",
    "fontScale=2: 약 2배 크게 보임\n",
    "\n",
    "더 크게 하려면 3~5까지 올리면 되고, 실제 해상도(HD, FHD)에 따라 체감이 다릅니다.\n",
    "\n",
    "라인 굵기\n",
    "\n",
    "FONT_THICKNESS_DEFAULT: 텍스트 라인 두께 (1이면 얇고, 2~3이면 두꺼워짐)\n",
    "\n",
    "BBOX_LINE_THICKNESS: BBox 테두리 굵기\n",
    "\n",
    "폰트 종류\n",
    "\n",
    "OpenCV 내장: cv2.FONT_HERSHEY_SIMPLEX, cv2.FONT_HERSHEY_DUPLEX, cv2.FONT_HERSHEY_COMPLEX, 등.\n",
    "\n",
    "한글 폰트: 기본적으로 OpenCV 자체 폰트는 한글 지원이 안 됩니다. freetype 모듈 또는 PIL ImageDraw 등 대안을 써야 합니다.\n",
    "\n",
    "pretendard, 나눔고딕 등 .ttf 경로를 직접 로드하려면\n",
    "\n",
    "(방법1) cv2.freetype.createFreeType2()로 TTF 지정\n",
    "\n",
    "(방법2) Pillow로 텍스트 그려서 다시 NumPy 변환\n",
    "\n",
    "본 예시에서는 주석으로만 안내했습니다.\n",
    "\n",
    "하이퍼 파라미터 설명\n",
    "\n",
    "detect_threshold=0.3 → Confidence 점수 0.3 이상만 detection 통과.\n",
    "\n",
    "stable_time_window=1.0 → 최근 1초(=FPS×1.0프레임) 동안 score≥threshold가 일정 비율이면 최종 통과.\n",
    "\n",
    "required_pass_ratio=0.3 → 윈도우 내 30% 이상이 threshold 이상 점수를 가져야 “score_passed=True”.\n",
    "\n",
    "다각형 구역 알람\n",
    "\n",
    "ENABLE_REGION_ALARM=True이면, TARGET_CLASS_FOR_ALARM이 polygon 내부 존재 시 상단 경고.\n",
    "\n",
    "bbox 중심점이 폴리곤 내부인지 검사 → is_point_in_polygon().\n",
    "\n",
    "경고 문구(ALARM_TEXT), 색(ALARM_TEXT_COLOR), 크기(ALARM_TEXT_SCALE), 위치(ALARM_POSITION) 조절 가능.\n",
    "\n",
    "에러 가능성/확장\n",
    "\n",
    "“트랙 사라지면 track_dict에서 제거” 로직 미구현. (메모리 증가 우려)\n",
    "\n",
    "“폴리곤 ROI를 GUI에서 마우스 클릭” → 여기서는 서버 환경이라 배제.\n",
    "\n",
    "“다양한 클래스들 간 앙상블 병합” → 필요시 custom_ensemble_detections()를 더 확장하면 됨.\n",
    "\n",
    "코드 충돌/변수 연동\n",
    "\n",
    "draw_text_with_options와 draw_detections가 전역 FONT/BBOX_LINE_THICKNESS를 잘 참조함.\n",
    "\n",
    "모델 로드 시, LOADED_MODELS dict에 잘 들어가고, process_video 내에서 이 dict를 사용함.\n",
    "\n",
    "Polygon ROI 체크 함수와 실제 알람 로직에서 동일 POLYGON_COORDS를 참조.\n",
    "\n",
    "마지막 **[Cell 10]**에서 “debug_polygon()”를 호출해, 임의 이미지로 폴리곤 디버깅을 할 수 있도록 했습니다.\n",
    "\n",
    "이상으로 전체 코드를 제시하고, 새로 추가된 기능(폰트 크기/종류/라인, 다각형 알람 등)에 대해 자세히 설명했습니다.\n",
    "추가로 ROI를 여러 개 두거나, 알람 대상 클래스를 여러 개로 하는 등의 확장은 동일한 방식으로 가능하오니, 필요시 로직을 조금씩 수정해 사용하시면 됩니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# %% [markdown]\n",
    "# # (Ⅰ) 전역 설정 + 라이브러리 로드\n",
    "# \n",
    "# 본 노트북에서는 4개 모델(whhb, con8, harness, fall)을 동시에\n",
    "# 앙상블/트래킹 + 클래스별 파라미터/시각화 on/off를 수행하고,\n",
    "# 폰트 설정(크기, 종류, 라인굵기), BBox 라인굵기, \n",
    "# 그리고 특정 다각형 구역에 특정 클래스가 들어왔을 때 상단 경고를 띄우는 기능(on/off)을 추가합니다.\n",
    "\n",
    "# %% [Cell 1] 전역 설정 (하이퍼파라미터, 폰트, 알람 구역 등)\n",
    "import os\n",
    "import sys\n",
    "import time\n",
    "import glob\n",
    "import cv2\n",
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "from loguru import logger\n",
    "from types import SimpleNamespace\n",
    "from tqdm import tqdm\n",
    "import mmcv\n",
    "import gc\n",
    "import torch\n",
    "import torch.nn as nn\n",
    "\n",
    "from mmengine.registry import init_default_scope\n",
    "from mmdet.apis import inference_detector, init_detector\n",
    "from mmdet.utils import register_all_modules\n",
    "from yolox.tracker.byte_tracker import BYTETracker\n",
    "from yolox.tracking_utils.timer import Timer\n",
    "\n",
    "#### (!!!) 자주 바꿀 수 있는 글로벌 파라미터 ####\n",
    "\n",
    "# 1) 경로 설정\n",
    "BASE_DIR = \"/DATA2/ltb\"  # (예시) '/DATA2'는 실제 디스크 위치\n",
    "VIDEO_DIR = \"/ltb/media/ltb/90887173887158A4/Users/ltb/회사용/4090_옮길거/val_sample/(박스)it1_현대_테스트할영상_편집본_모음\"   # 추론할 영상 폴더 절대경로\n",
    "SAVE_RESULT_BASE = \"/ltb/media/ltb/90887173887158A4/Users/ltb/회사용/4090_옮길거/val_sample/output_val_samples/(박스)it1_현대_테스트할영상_편집본_모음_위험구역_전체테스트_01\"  # 결과 저장 베이스 경로\n",
    "SAVE_SUBFOLDER = \"exp4_polygon_alarm\"  # exp 폴더명 증가\n",
    "SAVE_RESULT = os.path.join(SAVE_RESULT_BASE, SAVE_SUBFOLDER)\n",
    "os.makedirs(SAVE_RESULT, exist_ok=True)\n",
    "\n",
    "print(\"✅ SAVE_RESULT =\", SAVE_RESULT)\n",
    "\n",
    "# 2) 모델 목록 (4개)\n",
    "#    - config/checkpoint/class_names/params\n",
    "MODELS_CONFIG = {\n",
    "    # 예시) whhb\n",
    "    \"whhb\": {\n",
    "        \"config\": \"/ltb/media/ltb/90887173887158A4/Users/ltb/회사용/4090_옮길거/mmyolo/work_dirs/it1_현대_영상용_weight모음/whh/it1_250326_1819_4class_rota_resize_0p5_1p0_2p0_5p0.py\",\n",
    "        \"checkpoint\": \"/ltb/media/ltb/90887173887158A4/Users/ltb/회사용/4090_옮길거/mmyolo/work_dirs/it1_현대_영상용_weight모음/whh/best_coco_bbox_mAP_epoch_12.pth\",\n",
    "        \"class_names\": [\"worker\",\"helmet\",\"head\",\"background\"],\n",
    "        \"do_inference\": True,\n",
    "        \"vis_on\": True,\n",
    "        \"class_params\": {\n",
    "            \"default\": {\n",
    "                \"detect_threshold\": 0.0,\n",
    "                \"stable_time_window\": 1.0,\n",
    "                \"required_pass_ratio\": 0.3\n",
    "            },\n",
    "            \"worker\": {\n",
    "                \"detect_threshold\": 0.4,\n",
    "                \"stable_time_window\": 1.0,\n",
    "                \"required_pass_ratio\": 0.3\n",
    "            },\n",
    "            \"helmet\": {\n",
    "                \"detect_threshold\": 0.0,\n",
    "                \"stable_time_window\": 1.0,\n",
    "                \"required_pass_ratio\": 0.3\n",
    "            },\n",
    "            \"head\": {\n",
    "                \"detect_threshold\": 0.0,\n",
    "                \"stable_time_window\": 1.0,\n",
    "                \"required_pass_ratio\": 0.3\n",
    "            }\n",
    "            # 필요시 helmet, head 등 추가\n",
    "        }\n",
    "    },\n",
    "    \"fall\": {\n",
    "        \"config\": \"/ltb/media/ltb/90887173887158A4/Users/ltb/회사용/4090_옮길거/mmyolo/work_dirs/it1_현대_영상용_weight모음/fall전용/yolov8_l_960_custom_fall.py\",\n",
    "        \"checkpoint\": \"/ltb/media/ltb/90887173887158A4/Users/ltb/회사용/4090_옮길거/mmyolo/work_dirs/it1_현대_영상용_weight모음/fall전용/best_coco_bbox_mAP_epoch_15.pth\",\n",
    "        \"class_names\": [\"worker\",\"background\"],\n",
    "        \"do_inference\": True,\n",
    "        \"vis_on\": True,\n",
    "        \"class_params\": {\n",
    "            \"default\": {\n",
    "                \"detect_threshold\": 0.0,\n",
    "                \"stable_time_window\": 1.0,\n",
    "                \"required_pass_ratio\": 0.1\n",
    "            },\n",
    "            \"worker\": {\n",
    "                \"detect_threshold\": 0.1,\n",
    "                \"stable_time_window\": 1.0,\n",
    "                \"required_pass_ratio\": 0.3\n",
    "            }\n",
    "        }\n",
    "    },\n",
    "    \"con8\": {\n",
    "        \"config\": \"/ltb/media/ltb/90887173887158A4/Users/ltb/회사용/4090_옮길거/mmyolo/work_dirs/it1_현대_영상용_weight모음/con8/yolo8_large_960size_con_equip_8class.py\",\n",
    "        \"checkpoint\": \"/ltb/media/ltb/90887173887158A4/Users/ltb/회사용/4090_옮길거/mmyolo/work_dirs/it1_현대_영상용_weight모음/con8/best_coco_bbox_mAP_epoch_12.pth\",\n",
    "        \"class_names\": [\"doger\",\"scissor_lift\",\"evacavator\",\"dump_truck\",\"mixer_truck\",\"crane_mobile\",\"cargo_truck\",\"forklift\"],\n",
    "        \"do_inference\": True,\n",
    "        \"vis_on\": True,\n",
    "        \"class_params\": {\n",
    "            \"default\": {\n",
    "                \"detect_threshold\": 0.0,\n",
    "                \"stable_time_window\": 1.0,\n",
    "                \"required_pass_ratio\": 0.05\n",
    "            }\n",
    "        }\n",
    "    },\n",
    "    \"harness\": {\n",
    "        \"config\": \"...\",\n",
    "        \"checkpoint\": \"...\",\n",
    "        \"class_names\": [\"worker\",\"helmet\",\"harness\",\"signalman\",\"mixer_truck\",\"excavator\"],\n",
    "        \"do_inference\": False,\n",
    "        \"vis_on\": False,\n",
    "        \"class_params\": {}\n",
    "    }\n",
    "}\n",
    "\n",
    "# 3) 앙상블 모드 (union/priority) - 헬멧 vs head, whhb_worker vs fall_worker 등은 추가 로직\n",
    "ENSEMBLE_STRATEGY = \"union\"\n",
    "\n",
    "# 4) ByteTracker 파라미터\n",
    "TRACK_THRESH = 0.05\n",
    "TRACK_BUFFER = 300\n",
    "MATCH_THRESH = 0.95\n",
    "\n",
    "# 5) 시각화 폰트 & BBox 라인 굵기\n",
    "#    * OpenCV 폰트 크기는 실제 픽셀 단위로 잘못 이해될 수 있음\n",
    "#    * pt 단위로 직접 지정 불가. 여기서는 \"FONT_SCALE\"로 가정.\n",
    "#    * 예) FONT_SCALE=1 ~ 2 : 상대적으로 작은 크기\n",
    "#    *     FONT_SCALE=3 ~ 5 : 매우 큰 크기\n",
    "FONT_SCALE_DEFAULT = 1   # 폰트 스케일(예: 2면 꽤 크게 보임)\n",
    "FONT_THICKNESS_DEFAULT = 1  # 글씨 라인 굵기\n",
    "BBOX_LINE_THICKNESS = 2   # 박스 선 굵기\n",
    "\n",
    "# [수정] 폰트 종류\n",
    "#  - 예) cv2.FONT_HERSHEY_SIMPLEX, cv2.FONT_HERSHEY_DUPLEX, ...\n",
    "#  - 한글 폰트(예: Pretendard, 나눔고딕) 사용 시, \n",
    "#    OpenCV default font_face로는 제대로 표시가 안 될 수 있음.\n",
    "#    → freetype/pil로 별도 그려야 함(본 예시에서는 cv2 폰트만)\n",
    "USE_CUSTOM_FONT_FACE = True\n",
    "CUSTOM_FONT_FACE = cv2.FONT_HERSHEY_DUPLEX  # 다른 예: cv2.FONT_HERSHEY_SIMPLEX\n",
    "# 한글 폰트를 쓰려면? → OpenCV에 freetype 모듈 세팅 or PIL 사용 → 예시 주석:\n",
    "#   import freetype cv2 plugin or PIL draw\n",
    "#   myttf=\"/path/to/Pretendard.ttf\"\n",
    "\n",
    "# 6) 색상 팔레트 (worker=초록, helmet=갈색, head=빨강, 중장비=보라, 그 외 20개 리스트)\n",
    "COLOR_PALETTE = [\n",
    "    (0,255,0), (42,42,165), (0,0,255), (211,0,148), # 4개\n",
    "    (255,0,0), (255,255,0), (0,255,255), (255,0,255),\n",
    "    (100,100,100), (128,128,0), (128,0,128), (0,128,128),\n",
    "    (128,128,255), (128,255,128), (255,128,128),\n",
    "    (192,192,192), (0,0,128), (128,0,0),\n",
    "    (128,255,255), (255,128,255)\n",
    "]\n",
    "\n",
    "# 7) whhb_worker & fall_worker 앙상블 가중치 + IoU 임계값\n",
    "WHHB_FALL_WORKER_IOU_THR = 0.2\n",
    "WHHB_FALL_WORKER_SCORE_WEIGHT_WHHB = 0.7\n",
    "WHHB_FALL_WORKER_SCORE_WEIGHT_FALL = 0.6\n",
    "\n",
    "# helmet & head 앙상블\n",
    "HELMET_HEAD_IOU_THR = 0.2\n",
    "HELMET_SCORE_WEIGHT = 0.8\n",
    "HEAD_SCORE_WEIGHT = 0.4\n",
    "\n",
    "# 8) 다각형 구역 알람 기능\n",
    "ENABLE_REGION_ALARM = True   # True면 경고알람 기능 on\n",
    "TARGET_CLASS_FOR_ALARM = \"worker\"  # 어떤 클래스 발견 시 알람 띄울지\n",
    "POLYGON_COORDS = [\n",
    "    (500,500), (1000,500), (1200,1000), (700,1000)\n",
    "]  # 예시: 사각형. GUI 없는 서버이므로 좌표로 지정\n",
    "# 만약 마우스 클릭해서 ROI를 정하고 싶으면, \n",
    "# 별도 GUI가 필요(본 예시에서는 주석으로만 시뮬레이션).\n",
    "\n",
    "ALARM_TEXT = \"WARNING: Worker in restricted area!\"  # 알람 문구\n",
    "ALARM_TEXT_COLOR = (0,0,255)  # 빨강\n",
    "ALARM_TEXT_SCALE = 2\n",
    "ALARM_TEXT_THICKNESS = 2\n",
    "ALARM_POSITION = (50,50)   # 영상 상단 좌측에 경고\n",
    "\n",
    "print(\"===== [Cell 1] 전역 설정 완료 =====\")\n",
    "\n",
    "# %% [markdown]\n",
    "# [Cell 1] 종합:\n",
    "# - 위 파라미터를 조정하면,\n",
    "#   - 모델 config/체크포인트,\n",
    "#   - 폰트 크기/종류,\n",
    "#   - 앙상블 가중치,\n",
    "#   - 다각형 ROI 알람 기능 on/off,\n",
    "#   - 알람 텍스트 등\n",
    "#   를 한 번에 변경 가능.\n",
    "#\n",
    "# 디버깅 시, print문이나 예시로 shape, dtype 등을 확인해 봅시다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# %% [Cell 2] 헬퍼 함수 (IoU, distance, color 등)\n",
    "def iou_calc(a, b):\n",
    "    \"\"\"\n",
    "    두 bbox (x1,y1,x2,y2) 간 IoU 계산\n",
    "    - 0 <= IoU <= 1\n",
    "    \"\"\"\n",
    "    inter_x1 = max(a[0], b[0])\n",
    "    inter_y1 = max(a[1], b[1])\n",
    "    inter_x2 = min(a[2], b[2])\n",
    "    inter_y2 = min(a[3], b[3])\n",
    "    inter_w = max(0, inter_x2 - inter_x1)\n",
    "    inter_h = max(0, inter_y2 - inter_y1)\n",
    "    inter_area = inter_w * inter_h\n",
    "    area_a = (a[2]-a[0])*(a[3]-a[1])\n",
    "    area_b = (b[2]-b[0])*(b[3]-b[1])\n",
    "    union = area_a + area_b - inter_area\n",
    "    if union<=0:\n",
    "        return 0.0\n",
    "    return inter_area/union\n",
    "\n",
    "def distance(a, b):\n",
    "    \"\"\" 두 점 (x1,y1), (x2,y2) 사이 거리 \"\"\"\n",
    "    return np.hypot(a[0]-b[0], a[1]-b[1])\n",
    "\n",
    "def get_color_for_class(cname):\n",
    "    \"\"\"\n",
    "    특정 클래스명 -> 특정 색(BGR) 반환\n",
    "     worker=초록, helmet=갈색, head=빨강, 중장비=보라\n",
    "     그 외=COLOR_PALETTE[0]\n",
    "    \"\"\"\n",
    "    if cname==\"worker\":\n",
    "        return (0,255,0)   # 초록\n",
    "    if cname==\"helmet\":\n",
    "        return (42,42,165) # 갈색\n",
    "    if cname==\"head\":\n",
    "        return (0,0,255)   # 빨강\n",
    "    if cname in [\"doger\",\"scissor_lift\",\"evacavator\",\"dump_truck\",\n",
    "                 \"mixer_truck\",\"crane_mobile\",\"cargo_truck\",\"forklift\",\"excavator\"]:\n",
    "        return (211,0,148) # 보라\n",
    "\n",
    "    # 그 외\n",
    "    return COLOR_PALETTE[0]\n",
    "\n",
    "def is_point_in_polygon(point, polygon):\n",
    "    \"\"\"\n",
    "    ray-casting or winding 방식으로 점이 폴리곤 내부인지 판단\n",
    "    여기서는 cv2.pointPolygonTest()를 써도 됨\n",
    "    \"\"\"\n",
    "    cnt = np.array(polygon, dtype=np.int32)\n",
    "    # cv2.pointPolygonTest() -> 양수이면 내부, 0이면 경계, 음수이면 외부\n",
    "    val = cv2.pointPolygonTest(cnt, point, False)\n",
    "    return (val>=0)\n",
    "\n",
    "print(\"===== [Cell 2] 헬퍼 함수 정의 완료 =====\")\n",
    "\n",
    "# %% [markdown]\n",
    "# [Cell 2]에서는 IoU 계산, 거리 계산, 색상 매핑, point in polygon 등\n",
    "# 헬퍼 함수를 정의합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# %% [Cell 3] 모델 로드\n",
    "LOADED_MODELS = {}\n",
    "def load_all_models():\n",
    "    \"\"\"\n",
    "    MODELS_CONFIG를 순회하며 do_inference=True인 모델만 로드\n",
    "    \"\"\"\n",
    "    loaded = {}\n",
    "    for mk, mkinfo in MODELS_CONFIG.items():\n",
    "        if not mkinfo[\"do_inference\"]:\n",
    "            print(f\"[모델 스킵] {mk} -> do_inference=False\")\n",
    "            continue\n",
    "        print(f\"[모델 로드] {mk}\")\n",
    "        cfg = mkinfo[\"config\"]\n",
    "        ckp = mkinfo[\"checkpoint\"]\n",
    "        print(f\"    config={cfg}\")\n",
    "        print(f\"    checkpoint={ckp}\")\n",
    "        model_obj = init_detector(cfg, ckp, device=\"cuda\" if torch.cuda.is_available() else \"cpu\")\n",
    "        model_obj.dataset_meta = {\"CLASSES\": mkinfo[\"class_names\"]}\n",
    "        loaded[mk] = model_obj\n",
    "    return loaded\n",
    "\n",
    "LOADED_MODELS = load_all_models()\n",
    "\n",
    "print(\"===== [Cell 3] 모델 로드 완료 =====\")\n",
    "\n",
    "# %% [markdown]\n",
    "# [Cell 3]에서 실제 모델들을 로드합니다.\n",
    "# - do_inference=False인 모델은 스킵"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# %% [Cell 4] 디텍션 & 시각화 함수 (폰트/라인 굵기 조절)\n",
    "def get_class_param(model_key, class_label, param_key):\n",
    "    \"\"\"\n",
    "    MODELS_CONFIG[model_key][\"class_params\"][class_label or default][param_key]\n",
    "    \"\"\"\n",
    "    if model_key not in MODELS_CONFIG:\n",
    "        return None\n",
    "    cparams = MODELS_CONFIG[model_key].get(\"class_params\", {})\n",
    "    if class_label in cparams:\n",
    "        return cparams[class_label].get(param_key, cparams.get(\"default\",{}).get(param_key, None))\n",
    "    else:\n",
    "        return cparams.get(\"default\",{}).get(param_key, None)\n",
    "\n",
    "def detect_objects_single_model(frame, model_key, model_obj):\n",
    "    \"\"\"\n",
    "    모델별 detect_threshold 적용\n",
    "    \"\"\"\n",
    "    class_names = MODELS_CONFIG[model_key][\"class_names\"]\n",
    "    result = inference_detector(model_obj, frame)\n",
    "    if not hasattr(result, \"pred_instances\"):\n",
    "        return []\n",
    "    bboxes = result.pred_instances.bboxes\n",
    "    scores = result.pred_instances.scores\n",
    "    labels = result.pred_instances.labels\n",
    "    detections=[]\n",
    "    for i in range(len(bboxes)):\n",
    "        cid = labels[i].item()\n",
    "        if cid>=len(class_names):\n",
    "            continue\n",
    "        cname = class_names[cid]\n",
    "        sc = scores[i].item()\n",
    "        box = bboxes[i].tolist()\n",
    "        thr = get_class_param(model_key, cname, \"detect_threshold\")\n",
    "        if thr is None: thr=0.0\n",
    "        if sc<thr:\n",
    "            continue\n",
    "        detections.append({\n",
    "            \"bbox\": box,\n",
    "            \"score\": sc,\n",
    "            \"label\": cname,\n",
    "            \"model_key\": model_key\n",
    "        })\n",
    "    return detections\n",
    "\n",
    "def draw_text_with_options(\n",
    "    image, text_lines, anchor_point, text_position=\"top\",\n",
    "    font_scale=None, font_thickness=None, color=(0,255,0),\n",
    "    auto_adjust=True\n",
    "):\n",
    "    \"\"\"\n",
    "    OpenCV Text 그리기. font_scale과 font_thickness는 None이면 전역값 사용\n",
    "    \"\"\"\n",
    "    if font_scale is None:\n",
    "        font_scale = FONT_SCALE_DEFAULT\n",
    "    if font_thickness is None:\n",
    "        font_thickness = FONT_THICKNESS_DEFAULT\n",
    "\n",
    "    if USE_CUSTOM_FONT_FACE:\n",
    "        font_face = CUSTOM_FONT_FACE\n",
    "    else:\n",
    "        font_face = cv2.FONT_HERSHEY_SIMPLEX\n",
    "\n",
    "    x_anchor, y_anchor = anchor_point\n",
    "    for i, line in enumerate(text_lines):\n",
    "        text_size, _ = cv2.getTextSize(line, font_face, font_scale, font_thickness)\n",
    "        text_w, text_h = text_size\n",
    "        offset = i*(text_h+5)\n",
    "\n",
    "        if text_position==\"top\":\n",
    "            text_org = (x_anchor, y_anchor - 5 - offset)\n",
    "        elif text_position==\"bottom\":\n",
    "            text_org = (x_anchor, y_anchor + text_h + 5 + offset)\n",
    "        elif text_position==\"left\":\n",
    "            text_org = (x_anchor - text_w - 5, y_anchor + text_h + offset)\n",
    "        else:\n",
    "            text_org = (x_anchor + 5, y_anchor + text_h + offset)\n",
    "\n",
    "        if auto_adjust:\n",
    "            if text_org[0]<0:\n",
    "                text_org=(5, text_org[1])\n",
    "            elif text_org[0]+text_w>image.shape[1]:\n",
    "                text_org=(image.shape[1]-text_w-5, text_org[1])\n",
    "            if text_org[1]-text_h<0:\n",
    "                text_org=(text_org[0], text_h+5)\n",
    "            elif text_org[1]>image.shape[0]:\n",
    "                text_org=(text_org[0], image.shape[0]-5)\n",
    "\n",
    "        cv2.putText(image, line, text_org, font_face, font_scale, color, font_thickness)\n",
    "\n",
    "def draw_detections(frame, detections):\n",
    "    \"\"\"\n",
    "    detections = [{ 'bbox':[x1,y1,x2,y2], 'score':..., 'label':..., 'track_id':..., 'model_key':... }, ...]\n",
    "    \"\"\"\n",
    "    output = frame.copy()\n",
    "    for det in detections:\n",
    "        box = det[\"bbox\"]\n",
    "        x1, y1, x2, y2 = [int(v) for v in box]\n",
    "        sc = det.get(\"score\", 0.0)\n",
    "        lbl= det.get(\"label\",\"unknown\")\n",
    "        mk= det.get(\"model_key\",\"?\")\n",
    "        tid=det.get(\"track_id\",None)\n",
    "\n",
    "        # 모델 시각화 on/off\n",
    "        if mk in MODELS_CONFIG:\n",
    "            if not MODELS_CONFIG[mk].get(\"vis_on\",False):\n",
    "                continue\n",
    "\n",
    "        # bbox\n",
    "        color = get_color_for_class(lbl)\n",
    "        cv2.rectangle(output,(x1,y1),(x2,y2), color, BBOX_LINE_THICKNESS)\n",
    "\n",
    "        # text\n",
    "        lines=[]\n",
    "        lines.append(f\"{mk}:{lbl}\")\n",
    "        lines.append(f\"{sc:.2f}\")\n",
    "        if tid is not None:\n",
    "            lines.append(f\"ID:{tid}\")\n",
    "\n",
    "        draw_text_with_options(\n",
    "            image=output,\n",
    "            text_lines=lines,\n",
    "            anchor_point=(x1,y1),\n",
    "            text_position=\"top\",\n",
    "            color=color\n",
    "        )\n",
    "\n",
    "    return output\n",
    "\n",
    "print(\"===== [Cell 4] 디텍션 & 시각화 함수 정의 완료 =====\")\n",
    "\n",
    "# %% [markdown]\n",
    "# [Cell 4]에서는 디텍션(`detect_objects_single_model`)과 시각화(`draw_detections`)를 정의합니다.\n",
    "# - 폰트 크기/두께, BBox 라인두께 등을 전역 변수로 제어.\n",
    "# - 폰트 종류도 on/off로 선택."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# %% [Cell 5] 다각형 구역(ROI) 설정 + 중간 디버깅\n",
    "import matplotlib\n",
    "matplotlib.use('Agg') # GUI 없는 환경\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "def check_polygon_region_and_debug(frame, polygon_coords, debug_save_path):\n",
    "    \"\"\"\n",
    "    [기능]\n",
    "    1) polygon_coords로 다각형을 그림\n",
    "    2) 임의의 점을 찍어보거나, frame에 윤곽선 표출 후 저장\n",
    "    3) ENABLE_REGION_ALARM on인 경우, polygon 내부에 TARGET_CLASS_FOR_ALARM이 있다면 알람\n",
    "       (단, 여기서는 디버그용, 실제 알람은 process_video에서)\n",
    "    \"\"\"\n",
    "    debug_img = frame.copy()\n",
    "\n",
    "    # polygon 그리기 (BGR)\n",
    "    pts = np.array([polygon_coords], dtype=np.int32)\n",
    "    cv2.polylines(debug_img, pts, isClosed=True, color=(0,255,255), thickness=2)\n",
    "\n",
    "    # 저장 및 matplotlib 표시\n",
    "    cv2.imwrite(debug_save_path, debug_img)\n",
    "    print(f\"[DEBUG] polygon check image saved => {debug_save_path}\")\n",
    "\n",
    "    # 만약 ipynb에서 plt.imshow()로도 보고싶다면:\n",
    "    # (GUI 없는 리눅스환경에서도 inline 가능)\n",
    "    img_rgb = cv2.cvtColor(debug_img, cv2.COLOR_BGR2RGB)\n",
    "    plt.figure(figsize=(5,5))\n",
    "    plt.imshow(img_rgb)\n",
    "    plt.title(\"Debug Polygon\")\n",
    "    plt.axis('off')\n",
    "    # 저장\n",
    "    debug_save_path_png = debug_save_path+\".png\"\n",
    "    plt.savefig(debug_save_path_png)\n",
    "    print(f\"[DEBUG] polygon check matplotlib => {debug_save_path_png}\")\n",
    "\n",
    "print(\"===== [Cell 5] Polygon ROI 설정 + 디버그 준비 완료 =====\")\n",
    "\n",
    "# %% [markdown]\n",
    "# [Cell 5]에서 다각형 구역을 그린 뒤 중간 디버깅 이미지를 저장해볼 수 있는 함수를 정의했습니다.\n",
    "# - 실제 알람 로직은 영상 처리 과정([Cell 8])에서 이루어집니다.\n",
    "# - 여기서는 임의 프레임 하나를 불러서 polygon이 잘 그려지는지 확인용."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# %% [Cell 6] ByteTracker + 슬라이딩 윈도우\n",
    "def create_tracker():\n",
    "    return BYTETracker(SimpleNamespace(\n",
    "        track_thresh=TRACK_THRESH,\n",
    "        track_buffer=TRACK_BUFFER,\n",
    "        match_thresh=MATCH_THRESH\n",
    "    ))\n",
    "\n",
    "def update_track_states(tracks, track_dict, fps):\n",
    "    for t in tracks:\n",
    "        tid = t.track_id\n",
    "        x1,y1,w,h = t.tlwh\n",
    "        x2=x1+w\n",
    "        y2=y1+h\n",
    "        center_now=( (x1+x2)/2, (y1+y2)/2 )\n",
    "\n",
    "        if tid not in track_dict:\n",
    "            track_dict[tid]={\n",
    "                \"last_center\": None,\n",
    "                \"is_filtered\": False,\n",
    "                \"score_history\": [],\n",
    "                \"score_passed\": False,\n",
    "                \"class_label\": None,\n",
    "                \"model_key\": None\n",
    "            }\n",
    "\n",
    "        if track_dict[tid][\"last_center\"] is None:\n",
    "            track_dict[tid][\"last_center\"]=center_now\n",
    "        else:\n",
    "            dist_val = distance(center_now, track_dict[tid][\"last_center\"])\n",
    "            track_dict[tid][\"last_center\"]=center_now\n",
    "\n",
    "        if track_dict[tid][\"is_filtered\"]:\n",
    "            continue\n",
    "\n",
    "        # 슬라이딩 윈도우\n",
    "        sc = t.score\n",
    "        track_dict[tid][\"score_history\"].append(sc)\n",
    "\n",
    "        mk = track_dict[tid][\"model_key\"]\n",
    "        clbl = track_dict[tid][\"class_label\"]\n",
    "        if mk is None or clbl is None:\n",
    "            stable_tw=1.0\n",
    "            req_ratio=0.3\n",
    "            thr=0.0\n",
    "        else:\n",
    "            stable_tw = get_class_param(mk, clbl, \"stable_time_window\")\n",
    "            req_ratio = get_class_param(mk, clbl, \"required_pass_ratio\")\n",
    "            thr = get_class_param(mk, clbl, \"detect_threshold\")\n",
    "            if stable_tw is None: stable_tw=1.0\n",
    "            if req_ratio is None: req_ratio=0.3\n",
    "            if thr is None: thr=0.0\n",
    "\n",
    "        window_size=int(fps*stable_tw)\n",
    "        if window_size<1: window_size=1\n",
    "        hist = track_dict[tid][\"score_history\"]\n",
    "        if len(hist)>window_size:\n",
    "            track_dict[tid][\"score_history\"]=hist[-window_size:]\n",
    "\n",
    "        pass_count=sum([1 for s in track_dict[tid][\"score_history\"] if s>=thr])\n",
    "        ratio=pass_count/len(track_dict[tid][\"score_history\"]) if len(track_dict[tid][\"score_history\"])>0 else 0\n",
    "        if ratio>=req_ratio:\n",
    "            track_dict[tid][\"score_passed\"]=True\n",
    "\n",
    "print(\"===== [Cell 6] ByteTracker + 슬라이딩 윈도우 필터 완료 =====\")\n",
    "\n",
    "# %% [markdown]\n",
    "# [Cell 6] ByteTracker와 슬라이딩 윈도우 로직.\n",
    "# 점수 이력(history)로 required_pass_ratio를 넘으면 \"score_passed=True\"가 됩니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# %% [Cell 7] 앙상블 로직 (whhb_worker+fall_worker, helmet+head)\n",
    "def custom_ensemble_detections(dets):\n",
    "    \"\"\"\n",
    "    1) whhb:worker vs fall:worker => IoU>=WHHB_FALL_WORKER_IOU_THR => 점수 합산\n",
    "    2) helmet vs head => IoU>=HELMET_HEAD_IOU_THR => 가중치 비교, 큰쪽만\n",
    "    3) 그 외 => union\n",
    "    \"\"\"\n",
    "    used=[False]*len(dets)\n",
    "    final=[]\n",
    "    for i in range(len(dets)):\n",
    "        if used[i]:\n",
    "            continue\n",
    "        d1=dets[i]\n",
    "        x1,y1,x2,y2=d1[\"bbox\"]\n",
    "        sc1=d1[\"score\"]\n",
    "        lbl1=d1[\"label\"]\n",
    "        mk1=d1[\"model_key\"]\n",
    "\n",
    "        merged=None\n",
    "        for j in range(i+1,len(dets)):\n",
    "            if used[j]:\n",
    "                continue\n",
    "            d2=dets[j]\n",
    "            sc2=d2[\"score\"]\n",
    "            lbl2=d2[\"label\"]\n",
    "            mk2=d2[\"model_key\"]\n",
    "            iouv=iou_calc(d1[\"bbox\"],d2[\"bbox\"])\n",
    "\n",
    "            # whhb+fall => worker\n",
    "            if lbl1==\"worker\" and lbl2==\"worker\" and mk1==\"whhb\" and mk2==\"fall\" and iouv>=WHHB_FALL_WORKER_IOU_THR:\n",
    "                new_score = sc1*WHHB_FALL_WORKER_SCORE_WEIGHT_WHHB + sc2*WHHB_FALL_WORKER_SCORE_WEIGHT_FALL\n",
    "                # box는 d1꺼\n",
    "                merged={\n",
    "                    \"bbox\":d1[\"bbox\"],\n",
    "                    \"score\":new_score,\n",
    "                    \"label\":\"worker\",\n",
    "                    \"model_key\":\"whhb_fall\"\n",
    "                }\n",
    "                used[j]=True\n",
    "                break\n",
    "\n",
    "            # helmet vs head\n",
    "            if ((lbl1==\"helmet\" and lbl2==\"head\") or (lbl1==\"head\" and lbl2==\"helmet\")) and iouv>=HELMET_HEAD_IOU_THR:\n",
    "                sc1_w = sc1*HELMET_SCORE_WEIGHT if lbl1==\"helmet\" else sc1*HEAD_SCORE_WEIGHT\n",
    "                sc2_w = sc2*HELMET_SCORE_WEIGHT if lbl2==\"helmet\" else sc2*HEAD_SCORE_WEIGHT\n",
    "                if sc1_w>=sc2_w:\n",
    "                    used[j]=True\n",
    "                else:\n",
    "                    used[i]=True\n",
    "                break\n",
    "\n",
    "        if merged is not None:\n",
    "            final.append(merged)\n",
    "            used[i]=True\n",
    "        else:\n",
    "            if not used[i]:\n",
    "                final.append(d1)\n",
    "    return final\n",
    "\n",
    "print(\"===== [Cell 7] 앙상블 로직 완료 =====\")\n",
    "\n",
    "# %% [markdown]\n",
    "# [Cell 7]에서 whhb+fall worker 병합, helmet+head 우선순위 등 특별 로직을 구현.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# %% [Cell 8] 메인 처리 함수 (다각형알람 포함)\n",
    "def process_video(video_path, do_inference=True, do_save=True, resize_factor=1.0):\n",
    "    if not do_inference:\n",
    "        print(f\"[스킵] => {video_path}\")\n",
    "        return\n",
    "    base_name=os.path.basename(video_path)\n",
    "    name,ext=os.path.splitext(base_name)\n",
    "    output_video_path=os.path.join(SAVE_RESULT, f\"detected_{name}.mp4\")\n",
    "\n",
    "    cap=cv2.VideoCapture(video_path)\n",
    "    if not cap.isOpened():\n",
    "        print(f\"[오류] 열기실패 => {video_path}\")\n",
    "        return\n",
    "\n",
    "    fps=cap.get(cv2.CAP_PROP_FPS)\n",
    "    if fps<=0: fps=30\n",
    "    w_org=int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))\n",
    "    h_org=int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))\n",
    "    frame_count=int(cap.get(cv2.CAP_PROP_FRAME_COUNT))\n",
    "\n",
    "    if do_save:\n",
    "        fourcc=cv2.VideoWriter_fourcc(*'mp4v')\n",
    "        ww=int(w_org*resize_factor)\n",
    "        hh=int(h_org*resize_factor)\n",
    "        writer=cv2.VideoWriter(output_video_path, fourcc, fps, (ww,hh))\n",
    "        if not writer.isOpened():\n",
    "            print(f\"[경고] VideoWriter 생성실패 => {output_video_path}\")\n",
    "            writer=None\n",
    "    else:\n",
    "        writer=None\n",
    "\n",
    "    tracker=create_tracker()\n",
    "    track_dict={}\n",
    "    frame_times=[]\n",
    "    idx=0\n",
    "\n",
    "    # 알람 on/off\n",
    "    alarm_on = ENABLE_REGION_ALARM\n",
    "\n",
    "    print(f\"[INFO] 영상 처리 => {video_path}\")\n",
    "    start_time=time.time()\n",
    "\n",
    "    with tqdm(total=frame_count, desc=f\"Processing {base_name}\") as pbar:\n",
    "        while True:\n",
    "            st=time.time()\n",
    "            ret,frame=cap.read()\n",
    "            if not ret:\n",
    "                break\n",
    "\n",
    "            # (1) 여러 모델 detect\n",
    "            all_dets=[]\n",
    "            for mk,mobj in LOADED_MODELS.items():\n",
    "                det_single=detect_objects_single_model(frame,mk,mobj)\n",
    "                all_dets.extend(det_single)\n",
    "            # (2) 앙상블\n",
    "            final_dets=custom_ensemble_detections(all_dets)\n",
    "            # (3) ByteTracker\n",
    "            xyxyscore=[]\n",
    "            for d in final_dets:\n",
    "                x1,y1,x2,y2=d[\"bbox\"]\n",
    "                s=d[\"score\"]\n",
    "                xyxyscore.append([x1,y1,x2,y2,s])\n",
    "            xyxyscore_arr=np.array(xyxyscore) if len(xyxyscore)>0 else np.zeros((0,5),dtype=np.float32)\n",
    "            online_tracks=tracker.update(xyxyscore_arr,[h_org,w_org])\n",
    "            # (4) track_dict 라벨 지정\n",
    "            for t in online_tracks:\n",
    "                tid=t.track_id\n",
    "                x1,y1,w,h=t.tlwh\n",
    "                x2=x1+w\n",
    "                y2=y1+h\n",
    "                if tid not in track_dict:\n",
    "                    track_dict[tid]={\n",
    "                        \"last_center\":None,\n",
    "                        \"is_filtered\":False,\n",
    "                        \"score_history\":[],\n",
    "                        \"score_passed\":False,\n",
    "                        \"class_label\":None,\n",
    "                        \"model_key\":None\n",
    "                    }\n",
    "                if track_dict[tid][\"class_label\"] is not None:\n",
    "                    continue\n",
    "                best_iou=0\n",
    "                best_det=None\n",
    "                for d in final_dets:\n",
    "                    iov=iou_calc([x1,y1,x2,y2],d[\"bbox\"])\n",
    "                    if iov>best_iou:\n",
    "                        best_iou=iov\n",
    "                        best_det=d\n",
    "                if best_det and best_iou>=0.5:\n",
    "                    track_dict[tid][\"model_key\"]=best_det[\"model_key\"]\n",
    "                    track_dict[tid][\"class_label\"]=best_det[\"label\"]\n",
    "            # (5) 슬라이딩 윈도우\n",
    "            update_track_states(online_tracks, track_dict, fps)\n",
    "\n",
    "            # (6) 필터 통과\n",
    "            final_vis=[]\n",
    "            for t in online_tracks:\n",
    "                if not track_dict[t.track_id][\"score_passed\"]:\n",
    "                    continue\n",
    "                x1,y1,w,h=t.tlwh\n",
    "                x2=x1+w\n",
    "                y2=y1+h\n",
    "                mk=track_dict[t.track_id][\"model_key\"]\n",
    "                clbl=track_dict[t.track_id][\"class_label\"]\n",
    "                final_vis.append({\n",
    "                    \"bbox\":[x1,y1,x2,y2],\n",
    "                    \"score\":t.score,\n",
    "                    \"label\":clbl,\n",
    "                    \"track_id\":t.track_id,\n",
    "                    \"model_key\":mk\n",
    "                })\n",
    "            vis_frame=draw_detections(frame, final_vis)\n",
    "\n",
    "            # (7) 다각형 구역 알람\n",
    "            if alarm_on:\n",
    "                # final_vis 중에 TARGET_CLASS_FOR_ALARM이 polygon 내부 있으면 상단 경고\n",
    "                # polygon coords = POLYGON_COORDS\n",
    "                cnt = np.array([POLYGON_COORDS],dtype=np.int32)\n",
    "                # polygon 윤곽선 그리기\n",
    "                cv2.polylines(vis_frame, cnt, True, (255,255,0),2)\n",
    "                alarm_trigger=False\n",
    "                for dd in final_vis:\n",
    "                    if dd[\"label\"]==TARGET_CLASS_FOR_ALARM:\n",
    "                        # bbox center\n",
    "                        box=dd[\"bbox\"]\n",
    "                        bx1,by1,bx2,by2=box\n",
    "                        centerX=(bx1+bx2)/2\n",
    "                        centerY=(by1+by2)/2\n",
    "                        # polygon check\n",
    "                        if is_point_in_polygon((centerX,centerY), POLYGON_COORDS):\n",
    "                            alarm_trigger=True\n",
    "                            break\n",
    "                if alarm_trigger:\n",
    "                    draw_text_with_options(\n",
    "                        image=vis_frame,\n",
    "                        text_lines=[ALARM_TEXT],\n",
    "                        anchor_point=ALARM_POSITION,\n",
    "                        text_position=\"bottom\",\n",
    "                        font_scale=ALARM_TEXT_SCALE,\n",
    "                        font_thickness=ALARM_TEXT_THICKNESS,\n",
    "                        color=ALARM_TEXT_COLOR\n",
    "                    )\n",
    "\n",
    "            # (8) 리사이즈, 저장\n",
    "            if resize_factor!=1.0:\n",
    "                oh,ow=vis_frame.shape[:2]\n",
    "                vis_frame=cv2.resize(vis_frame,(int(ow*resize_factor),int(oh*resize_factor)))\n",
    "            if writer:\n",
    "                writer.write(vis_frame)\n",
    "\n",
    "            dt=time.time()-st\n",
    "            frame_times.append(dt)\n",
    "            pbar.update(1)\n",
    "\n",
    "        cap.release()\n",
    "        if writer: writer.release()\n",
    "\n",
    "    if frame_times:\n",
    "        avg_fps=1/np.mean(frame_times)\n",
    "        print(f\"[INFO] 평균 FPS => {avg_fps:.2f}\")\n",
    "    print(f\"[INFO] 결과 => {output_video_path}\")\n",
    "    print(f\"[INFO] 처리시간 => {time.time()-start_time:.1f} s\")\n",
    "\n",
    "print(\"===== [Cell 8] 메인 처리 함수 정의 완료 =====\")\n",
    "\n",
    "# %% [markdown]\n",
    "# [Cell 8]가 이 노트북의 핵심:\n",
    "# - 앙상블 + ByteTracker + 슬라이딩 윈도우 + (옵션) 다각형 구역 내 알람 표시\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# %% [Cell 9] 실행부 (main)\n",
    "def main():\n",
    "    exts={\".mp4\",\".avi\",\".mov\",\".mkv\"}\n",
    "    files=sorted(os.listdir(VIDEO_DIR))\n",
    "    for f in files:\n",
    "        base,ext=os.path.splitext(f)\n",
    "        if ext.lower() in exts:\n",
    "            vp=os.path.join(VIDEO_DIR,f)\n",
    "            process_video(vp,do_inference=True,do_save=True,resize_factor=1.0)\n",
    "        else:\n",
    "            print(f\"[스킵] 영상파일 아님 => {f}\")\n",
    "\n",
    "if __name__==\"__main__\":\n",
    "    print(\"===== [Cell 9] 실행부 start =====\")\n",
    "    main()\n",
    "    print(\"===== [Cell 9] 전체 영상 처리 완료 =====\")\n",
    "\n",
    "# %% [markdown]\n",
    "# [Cell 9]에서는 `main()` 함수를 통해 VIDEO_DIR 안의 영상을 모두 순회하며 추론합니다.\n",
    "\n",
    "# %% [markdown]\n",
    "# # (Ⅱ) 중간 디버깅 - 다각형 구역 테스트\n",
    "# 아래는 예시로 1프레임 또는 1이미지를 임의로 읽어서,\n",
    "# 다각형이 잘 그려지는지 확인할 수 있는 셀입니다.\n",
    "# 실제로는 GUI환경이 아니라서 폴리곤 좌표를 수작업으로 변경해야 합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# %% [Cell 10] 최종 확인 & 폴리곤 디버그\n",
    "def debug_polygon():\n",
    "    sample_img_path = SAVE_RESULT_BASE   # (!!!) 임의 이미지 경로\n",
    "    if not os.path.exists(sample_img_path):\n",
    "        print(\"[경고] sample_img 없음 => 스킵\")\n",
    "        return\n",
    "    img=cv2.imread(sample_img_path)\n",
    "    if img is None:\n",
    "        print(\"[오류] imread 실패 =>\", sample_img_path)\n",
    "        return\n",
    "    debug_save_path = os.path.join(SAVE_RESULT, \"debug_polygon.jpg\")\n",
    "    check_polygon_region_and_debug(img, POLYGON_COORDS, debug_save_path)\n",
    "\n",
    "debug_polygon()\n",
    "\n",
    "print(\"===== [Cell 10] 최종 디버깅 + 코드 검토 완료 =====\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "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": 4
}
