Files
pbl02/uploads/OPS01/OPS01_student.ipynb
T
2026-10-08 17:15:05 +09:00

2921 lines
178 KiB
Plaintext

{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# 🎯 [OPS01] 드론 영상 공중위협 객체 탐지와 오경보 검증\n",
"### Google Colab 실습 환경 (작전·전투 수행 / Level 3)\n",
"\n",
"[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/edunlab/milicontent2/blob/main/uploads/OPS01/OPS01_solution.ipynb)\n",
"\n",
"---\n",
"\n",
"## 📌 1. 프로젝트 개요 및 배경\n",
"\n",
"| 항목 | 내용 |\n",
"| :--- | :--- |\n",
"| **과정 분류** | 작전·전투 수행 (Level 3) |\n",
"| **학습 소요 시간** | 10개 미션 (5일 / 총 27.5시간) |\n",
"| **환경 사양** | Python 3.10+, numpy, pandas, matplotlib, scipy, scikit-learn |\n",
"\n",
"### 📖 프로젝트 목표\n",
"러시아-우크라이나 전쟁에서는 소형 드론이 정찰과 공격에 대량으로 쓰이면서, 영상으로 공중위협을 빨리 찾아내는 일만큼 조류·민간 항공기·지상 반사체 때문에 생기는 오경보를 줄이는 일이 중요해졌다. 본 프로젝트에서는 교육용 가상 적외선 영상을 코드로 직접 만들어, 데이터 스키마 정의, 기상 왜곡 전처리, 탐지 Baseline, 군집·급강하 패턴 식별, 신뢰도 점수 산출, 전술 상황판, 인간 개입(HITL) 승인/보류, 포화 공격 스트레스 시험, AAR 재보정, 최종 통합까지 대드론 탐지·경보 파이프라인을 단계별로 구현한다. 최종 목표는 드론을 놓치지 않으면서 오경보를 줄이고, 모든 교전 관련 판단이 사람의 승인을 거치도록 설계하는 것이다.\n",
"\n",
"> **⚠️ [교육용 가상 데이터셋 원칙]**\n",
"> - 본 노트북의 모든 데이터(제원, 로그, 시나리오 등)는 보안 규정을 준수하여 **코드 내 난수 기반 가상 데이터로 생성**됩니다.\n",
"> - 실제 부대 위치, 작전 인가, 실제 전장 정보, 군사 기밀 데이터는 일체 포함되지 않습니다.\n",
"\n",
"---\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# ============================================================================\n",
"# [Google Colab 환경 설정] 필수 라이브러리 설치 및 임포트\n",
"# ============================================================================\n",
"!pip install -q numpy pandas matplotlib scipy scikit-learn\n",
"\n",
"import os\n",
"import sys\n",
"import time\n",
"import math\n",
"import json\n",
"import re\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"import warnings\n",
"warnings.filterwarnings('ignore')\n",
"\n",
"# 그래프 영문 폰트 기본 설정 (환경 독립성 보장)\n",
"plt.rcParams['font.sans-serif'] = 'DejaVu Sans'\n",
"plt.rcParams['axes.unicode_minus'] = False\n",
"\n",
"print(f\"실행 환경 준비 완료 (Python {sys.version.split()[0]})\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## 🎯 미션 01. 전술 드론 영상 수집 환경 분석 및 공중위협 객체(드론/미사일) 데이터셋 스키마 정의\n",
"**소요시간**: 약 1.5시간\n",
"\n",
"### 📋 미션 배경 및 수행 지침\n",
"#### 미션 배경 및 당위성\n",
"\n",
"러시아-우크라이나 전쟁 전장 환경을 분석하여 드론 탑재 광학/적외선(EO/IR) 센서 규격, 공중위협 객체(정찰·자폭드론, 순항미사일) 메타데이터 및 바운딩 박스 라벨링 스키마를 설계하고 정의한다. 실제 전술 현장에서는 단순 탐지에 그치지 않고 신속한 위협 판단과 신뢰도 높은 의사결정이 필수적이므로, **드론 탑재 EO/IR 영상으로 공중위협을 탐지할 때 필요한 센서 규격, 전술 파라미터, 바운딩 박스 라벨 구조를 정의하고 잘못된 데이터를 걸러내는 검증기를 만든다.** 과업을 완수하여 오판을 방지하고 작전 대응력을 극대화해야 합니다.\n",
"\n",
"이 문제를 해결하기 위해 본 미션에서는 **정찰·자폭드론, 순항미사일, 조류, 민간기, 지상 반사체의 클래스 체계를 정하고 센서(EO/IR, 해상도, fps)와 전술 파라미터(기상, 위치오차, 통신지연) 필드를 포함한 스키마 설계하고 작성하는** 실습을 체계적으로 수행합니다.\n",
"\n",
"학습자는 실습가이드에 제시된 총 4개 세부 활동(기본 기능 구현 ➔ 결과 지표 검증 ➔ 파라미터 튜닝 및 도전과제)을 순차적으로 진행하며 핵심 판정 기준을 달성합니다.\n",
"\n",
"**이번 결과물:** `data/schema/ops01_input_schema.json 및 docs/ops01_data_spec.md` 형태의 결과물을 완성한다.\n",
"\n",
"### 🔍 평가 및 검증 기준 (PASS 조건)\n",
"#### 미션 판정\n",
"\n",
"**평가 구분:** 필수\n",
"\n",
"**핵심 평가영역:** 작전·전투 수행 실무 및 러시아-우크라이나 전쟁 전장 환경과의 군사적 부합성\n",
"\n",
"#### 미션 PASS 조건\n",
"\n",
"- 정량 기준: 스키마 필드 정의 완성도 100%, 샘플 데이터 검증 오류 0건\n",
"\n",
"\n",
"- 진행 기준: 입력 데이터 유효성 검증 스크립트 통과 및 명세서 문서화 완료\n",
"\n",
"\n",
"- 검증 방법: JSON Schema 검증 도구 및 샘플 데이터 파싱 단위 테스트\n",
"\n",
"\n",
"#### 채점자 판정 메모\n",
"\n",
"> 단순 코드 실행 여부뿐만 아니라 출력된 수치와 근거 자료가 군 실무 요구사항에 부합하는지 면밀히 확인한다.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"드론 탑재 EO/IR 영상의 공중위협 객체(정찰·자폭드론, 순항미사일) 메타데이터·라벨 스키마를 정의하고, 가상 샘플 데이터를 검증한다.\"\"\"\n",
"import json\n",
"import re\n",
"import numpy as np\n",
"import pandas as pd\n",
"\n",
"# 1) 스키마 정의: 센서 규격, 전술 파라미터(기상·위치오차·통신지연), 바운딩 박스 라벨\n",
"SCHEMA = {\n",
" \"required\": [\"sequence_id\", \"sensor\", \"tactical\", \"n_frames\", \"labels\"],\n",
" \"fields\": {\n",
" \"sequence_id\": {\"type\": str, \"pattern\": r\"^SEQ\\d{3}$\"},\n",
" \"n_frames\": {\"type\": int, \"min\": 1, \"max\": 3000},\n",
" },\n",
" \"sensor\": {\n",
" \"type\": {\"type\": str, \"enum\": [\"EO\", \"IR\"]},\n",
" \"resolution\": {\"type\": list, \"length\": 2},\n",
" \"fps\": {\"type\": (int, float), \"min\": 1, \"max\": 60},\n",
" },\n",
" \"tactical\": {\n",
" \"weather\": {\"type\": str, \"enum\": [\"clear\", \"night\", \"fog\", \"rain\", \"smoke\"]},\n",
" \"position_error_m\": {\"type\": (int, float), \"min\": 0, \"max\": 500},\n",
" \"comm_delay_ms\": {\"type\": (int, float), \"min\": 0, \"max\": 10000},\n",
" },\n",
" \"label\": {\n",
" \"class\": {\"type\": str, \"enum\": [\"recon_drone\", \"attack_drone\", \"cruise_missile\", \"bird\", \"civil_aircraft\", \"clutter\"]},\n",
" \"bbox\": {\"type\": list, \"length\": 4},\n",
" \"frame\": {\"type\": int, \"min\": 0},\n",
" \"thermal\": {\"type\": (int, float), \"min\": 0, \"max\": 255},\n",
" },\n",
"}\n",
"\n",
"\n",
"def check(name, value, rule):\n",
" \"\"\"값 하나를 규칙(type, enum, min, max, length, pattern)으로 검사해 오류 목록을 돌려준다.\"\"\"\n",
" # [TODO : 학생 실습 1] 전술 드론 영상 수집 환경 분석 및 공중위협 객체(드론/미사일) 데이터셋 스키마 정의 관련 핵심 로직을 구현하세요.\n",
" # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.\n",
" # <<< 여기에 코드를 작성하세요 >>>\n",
" pass # 작성 후 pass를 제거하세요.\n",
"def validate(rec):\n",
" # [TODO : 학생 실습 1] 전술 드론 영상 수집 환경 분석 및 공중위협 객체(드론/미사일) 데이터셋 스키마 정의 관련 핵심 로직을 구현하세요.\n",
" # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.\n",
" # <<< 여기에 코드를 작성하세요 >>>\n",
" pass # 작성 후 pass를 제거하세요.\n",
"rng = np.random.default_rng(1)\n",
"classes = SCHEMA[\"label\"][\"class\"][\"enum\"]\n",
"records = []\n",
"for i in range(20):\n",
" labels = [{\"class\": str(rng.choice(classes)), \"frame\": int(f),\n",
" \"bbox\": [int(rng.integers(0, 60)), int(rng.integers(0, 60)), 3, 3],\n",
" \"thermal\": float(round(rng.uniform(100, 240), 1))} for f in range(3)]\n",
" records.append({\"sequence_id\": f\"SEQ{i:03d}\", \"n_frames\": 30,\n",
" \"sensor\": {\"type\": str(rng.choice([\"EO\", \"IR\"])), \"resolution\": [64, 64], \"fps\": 10},\n",
" \"tactical\": {\"weather\": str(rng.choice([\"clear\", \"night\", \"fog\", \"rain\", \"smoke\"])),\n",
" \"position_error_m\": float(round(rng.uniform(2, 30), 1)),\n",
" \"comm_delay_ms\": int(rng.integers(20, 400))},\n",
" \"labels\": labels})\n",
"\n",
"# 3) 정상 데이터 검증\n",
"ok = sum(1 for r in records if not validate(r))\n",
"print(f\"정상 샘플 {len(records)}건 중 검증 통과 {ok}건, 오류 {len(records) - ok}건\")\n",
"\n",
"# 4) 오류 주입 시험: 검증기가 잘못된 데이터를 잡아내는지 확인\n",
"bad = json.loads(json.dumps(records[0]))\n",
"bad[\"sequence_id\"] = \"SEQ-1\"\n",
"bad[\"sensor\"][\"fps\"] = 120\n",
"del bad[\"tactical\"][\"comm_delay_ms\"]\n",
"bad[\"labels\"][0][\"class\"] = \"ufo\"\n",
"bad[\"labels\"][1][\"bbox\"] = [1, 2, 3]\n",
"print(\"\\n[오류 주입 결과]\")\n",
"for e in validate(bad):\n",
" print(\" -\", e)\n",
"\n",
"# 5) 스키마 필드 정의서(명세서 초안)\n",
"rows = [(\"기본\", k, r[\"type\"].__name__, str({x: y for x, y in r.items() if x != \"type\"})) for k, r in SCHEMA[\"fields\"].items()]\n",
"for g in (\"sensor\", \"tactical\", \"label\"):\n",
" for k, r in SCHEMA[g].items():\n",
" t = r[\"type\"].__name__ if isinstance(r[\"type\"], type) else \"number\"\n",
" rows.append((g, k, t, str({x: y for x, y in r.items() if x != \"type\"})))\n",
"spec = pd.DataFrame(rows, columns=[\"그룹\", \"필드\", \"자료형\", \"규칙\"])\n",
"print(\"\\n[데이터 명세 요약]\")\n",
"print(spec.to_string(index=False))\n",
"print(\"\\n클래스별 라벨 수:\", pd.Series([l[\"class\"] for r in records for l in r[\"labels\"]]).value_counts().to_dict())\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<details>\n",
"<summary><b>💡 [참고] 미션 01 예상 실행 결과 (Output)</b></summary>\n",
"\n",
"```text\n",
"정상 샘플 20건 중 검증 통과 20건, 오류 0건\n",
"\n",
"[오류 주입 결과]\n",
" - sequence_id: 형식 불일치 'SEQ-1'\n",
" - sensor.fps: 최대값 60 초과\n",
" - tactical.comm_delay_ms: 필수 필드 누락\n",
" - labels[0].class: 허용되지 않은 값 'ufo'\n",
" - labels[1].bbox: 길이 4 필요\n",
"\n",
"[데이터 명세 요약]\n",
" 그룹 필드 자료형 규칙\n",
" 기본 sequence_id str {'pattern': '^SEQ\\\\d{3}$'}\n",
" 기본 n_frames int {'min': 1, 'max': 3000}\n",
" sensor type str {'enum': ['EO', 'IR']}\n",
" sensor resolution list {'length': 2}\n",
" sensor fps number {'min': 1, 'max': 60}\n",
"tactical weather str {'enum': ['clear', 'night', 'fog', 'rain', 'smoke']}\n",
"tactical position_error_m number {'min': 0, 'max': 500}\n",
"tactical comm_delay_ms number {'min': 0, 'max': 10000}\n",
" label class str {'enum': ['recon_drone', 'attack_drone', 'cruise_missile', 'bird', 'civil_aircraft', 'clutter']}\n",
" label bbox list {'length': 4}\n",
" label frame int {'min': 0}\n",
" label thermal number {'min': 0, 'max': 255}\n",
"\n",
"클래스별 라벨 수: {'bird': 12, 'recon_drone': 12, 'cruise_missile': 10, 'clutter': 10, 'attack_drone': 8, 'civil_aircraft': 8}\n",
"```\n",
"</details>\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## 🎯 미션 02. 적외선·광학(EO/IR) 전장 영상 노이즈 제거 및 기상 왜곡 보정 전처리 모듈 구현\n",
"**소요시간**: 약 2.0시간\n",
"\n",
"### 📋 미션 배경 및 수행 지침\n",
"#### 미션 배경 및 당위성\n",
"\n",
"전장 악천후(안개, 연막, 강우) 및 센서 진동으로 발생하는 영상 노이즈를 필터링하고 명암비 강화(CLAHE) 및 프레임 정규화 전처리 파이프라인을 구축한다. 실제 전술 현장에서는 단순 탐지에 그치지 않고 신속한 위협 판단과 신뢰도 높은 의사결정이 필수적이므로, **안개·강우·야간 등 기상 왜곡과 센서 잡음이 섞인 영상을 조건별로 보정하는 전처리 파이프라인을 구축한다.** 과업을 완수하여 오판을 방지하고 작전 대응력을 극대화해야 합니다.\n",
"\n",
"이 문제를 해결하기 위해 본 미션에서는 **중간값 필터(잡음), 빗줄기 제거(가로 방향 형태학적 열림), 명암비 강화(CLAHE 방식), 0~1 정규화, 결측 프레임 보정을 구현하고 기상 조건별로 조합하여 최적화하는** 실습을 체계적으로 수행합니다.\n",
"\n",
"학습자는 실습가이드에 제시된 총 4개 세부 활동(기본 기능 구현 ➔ 결과 지표 검증 ➔ 파라미터 튜닝 및 도전과제)을 순차적으로 진행하며 핵심 판정 기준을 달성합니다.\n",
"\n",
"**이번 결과물:** `src/preprocess/ops01_preprocessor.py 및 전처리 전후 데이터셋` 형태의 결과물을 완성한다.\n",
"\n",
"### 🔍 평가 및 검증 기준 (PASS 조건)\n",
"#### 미션 판정\n",
"\n",
"**평가 구분:** 필수\n",
"\n",
"**핵심 평가영역:** 전술 환경 특유의 노이즈(스푸핑, 반사파, 센서 단절 등) 제거 타당성\n",
"\n",
"#### 미션 PASS 조건\n",
"\n",
"- 정량 기준: 데이터 정제 처리 속도 1,000건/초 이상, 전처리 오류율 0%\n",
"\n",
"\n",
"- 진행 기준: 정제 파이프라인 자동화 및 단위 테스트 케이스 5종 통과\n",
"\n",
"\n",
"- 검증 방법: pytest 기반 전처리 정합성 테스트 및 결측치 보정율 확인\n",
"\n",
"\n",
"#### 채점자 판정 메모\n",
"\n",
"> 단순 코드 실행 여부뿐만 아니라 출력된 수치와 근거 자료가 군 실무 요구사항에 부합하는지 면밀히 확인한다.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"안개·강우·야간 등 기상 왜곡과 센서 잡음이 섞인 EO/IR 영상을 조건별로 전처리(잡음 제거·빗줄기 제거·명암비 강화·정규화)하고, 처리 속도와 탐지 품질 개선 효과를 확인한다.\"\"\"\n",
"import time\n",
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
"from scipy import ndimage\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [교육용가상자료] 합성 적외선 영상 생성기 (실제 영상·부대 정보 없음)\n",
"# 64x64 프레임에 드론·조류·민간기·지상 반사체(클러터)를 그리고 정답 라벨을 함께 만든다.\n",
"# ---------------------------------------------------------------------------\n",
"H = W = 64\n",
"PROFILE = {\"drone\": ((2, 3), (170, 225)), \"bird\": ((2, 3), (110, 150)),\n",
" \"civil\": ((4, 6), (190, 240)), \"clutter\": ((2, 4), (140, 190))}\n",
"\n",
"\n",
"def _new_obj(cls, rng, dive=False, v=None, xy=None):\n",
" # [TODO : 학생 실습 2] 적외선·광학(EO/IR) 전장 영상 노이즈 제거 및 기상 왜곡 보정 전처리 모듈 구현 관련 핵심 로직을 구현하세요.\n",
" # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.\n",
" # <<< 여기에 코드를 작성하세요 >>>\n",
" pass # 작성 후 pass를 제거하세요.\n",
"def _noise(img, cond, rng):\n",
" # [TODO : 학생 실습 2] 적외선·광학(EO/IR) 전장 영상 노이즈 제거 및 기상 왜곡 보정 전처리 모듈 구현 관련 핵심 로직을 구현하세요.\n",
" # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.\n",
" # <<< 여기에 코드를 작성하세요 >>>\n",
" pass # 작성 후 pass를 제거하세요.\n",
"def make_sequence(seq_id, cond, rng, kind=\"mixed\", n_frames=30, n_drone=None, n_bird=None):\n",
" \"\"\"시퀀스 1개 = (프레임 배열, 라벨 목록). kind: mixed / swarm / dive / saturation\"\"\"\n",
" objs = []\n",
" if kind == \"swarm\":\n",
" v, cx, cy = rng.uniform(-1, 1, 2), rng.uniform(15, 35), rng.uniform(15, 35)\n",
" objs = [_new_obj(\"drone\", rng, v=v, xy=(cx + i * 5, cy + (i % 2) * 5)) for i in range(4)]\n",
" objs.append(_new_obj(\"bird\", rng))\n",
" elif kind == \"dive\":\n",
" d = _new_obj(\"drone\", rng, dive=True)\n",
" d[\"y\"] = float(rng.uniform(4, 14))\n",
" objs = [d, _new_obj(\"bird\", rng), _new_obj(\"clutter\", rng)]\n",
" elif kind == \"saturation\":\n",
" objs = [_new_obj(\"drone\", rng) for _ in range(n_drone)] + [_new_obj(\"bird\", rng) for _ in range(n_bird)]\n",
" else:\n",
" objs = [_new_obj(\"drone\", rng) for _ in range(int(rng.integers(1, 3)))]\n",
" objs += [_new_obj(\"bird\", rng) for _ in range(int(rng.integers(0, 3)))]\n",
" if rng.random() < 0.5:\n",
" objs.append(_new_obj(\"civil\", rng))\n",
" if rng.random() < 0.6:\n",
" objs.append(_new_obj(\"clutter\", rng))\n",
" frames, labels = np.zeros((n_frames, H, W), np.uint8), []\n",
" base = rng.normal(40, 6, (H, W))\n",
" for t in range(n_frames):\n",
" canvas = base.copy()\n",
" for i, o in enumerate(objs):\n",
" if t > 0:\n",
" if o[\"cls\"] == \"bird\":\n",
" o[\"vx\"] = float(np.clip(o[\"vx\"] + rng.normal(0, 0.45), -1.6, 1.6))\n",
" o[\"vy\"] = float(np.clip(o[\"vy\"] + rng.normal(0, 0.45), -1.6, 1.6))\n",
" if o[\"dive\"] and t > 10:\n",
" o[\"vy\"] += 0.18\n",
" o[\"x\"] = float(np.clip(o[\"x\"] + o[\"vx\"], 1, W - o[\"size\"] - 1))\n",
" o[\"y\"] = float(np.clip(o[\"y\"] + o[\"vy\"], 1, H - o[\"size\"] - 1))\n",
" heat = o[\"heat\"]\n",
" if o[\"cls\"] == \"clutter\" and rng.random() < 0.3:\n",
" heat *= 0.7\n",
" if o[\"cls\"] == \"bird\":\n",
" heat *= float(rng.uniform(0.85, 1.1))\n",
" x, y, s = int(round(o[\"x\"])), int(round(o[\"y\"])), o[\"size\"]\n",
" canvas[y:y + s, x:x + s] = np.maximum(canvas[y:y + s, x:x + s], heat)\n",
" labels.append({\"frame\": t, \"track\": f\"{seq_id}_T{i:02d}\", \"class\": o[\"cls\"], \"bbox\": [x, y, s, s],\n",
" \"pattern\": \"dive\" if o[\"dive\"] else (\"swarm\" if kind == \"swarm\" and o[\"cls\"] == \"drone\" else \"normal\")})\n",
" frames[t] = _noise(canvas, cond, rng)\n",
" return frames, labels\n",
"\n",
"\n",
"def make_dataset(n=24, seed=7):\n",
" rng = np.random.default_rng(seed)\n",
" conds = [\"clear\", \"night\", \"fog\", \"rain\"]\n",
" out = []\n",
" for i in range(n):\n",
" kind = \"swarm\" if i % 8 == 5 else (\"dive\" if i % 8 == 6 else \"mixed\")\n",
" f, lab = make_sequence(f\"SEQ{i:03d}\", conds[i % 4], rng, kind)\n",
" out.append({\"id\": f\"SEQ{i:03d}\", \"cond\": conds[i % 4], \"kind\": kind, \"frames\": f, \"labels\": lab})\n",
" return out\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"\n",
"def denoise(f, size):\n",
" return f if size <= 1 else ndimage.median_filter(f, size=size)\n",
"\n",
"\n",
"def remove_rain(f):\n",
" return ndimage.grey_opening(f, size=(1, 2))\n",
"\n",
"\n",
"def clahe_like(f, tiles=4, clip=0.02):\n",
" \"\"\"타일별 히스토그램 평활화 후 전체 대비 확장(간이 CLAHE). 안개로 떨어진 국소 대비를 살린다.\"\"\"\n",
" out = np.zeros_like(f, dtype=float)\n",
" th, tw = f.shape[0] // tiles, f.shape[1] // tiles\n",
" for i in range(tiles):\n",
" for j in range(tiles):\n",
" t = f[i * th:(i + 1) * th, j * tw:(j + 1) * tw].astype(float)\n",
" hist, edges = np.histogram(t, bins=64, range=(0, 256))\n",
" limit = max(1, int(clip * t.size))\n",
" hist = np.minimum(hist, limit)\n",
" cdf = np.cumsum(hist) / hist.sum()\n",
" out[i * th:(i + 1) * th, j * tw:(j + 1) * tw] = np.interp(t, edges[:-1], cdf) * 255\n",
" return 0.5 * out + 0.5 * stretch(f)\n",
"\n",
"\n",
"def stretch(f):\n",
" lo, hi = np.percentile(f, [1, 99.5])\n",
" return np.clip((f.astype(float) - lo) / (hi - lo + 1e-6) * 255, 0, 255)\n",
"\n",
"\n",
"def normalize(f):\n",
" return f.astype(np.float32) / 255.0\n",
"\n",
"\n",
"PARAMS = {\"clear\": dict(median=1, rain=False), \"night\": dict(median=1, rain=False),\n",
" \"fog\": dict(median=1, rain=False), \"rain\": dict(median=1, rain=True)}\n",
"\n",
"\n",
"def preprocess(frame, cond):\n",
" p = PARAMS[cond]\n",
" f = denoise(frame.astype(float), p[\"median\"])\n",
" if p[\"rain\"]:\n",
" f = remove_rain(f)\n",
" return stretch(f)\n",
"\n",
"\n",
"def quick_f1(seqs, use_pre, k=2.5):\n",
" tp = fp = fn = 0\n",
" for s in seqs:\n",
" for t, fr in enumerate(s[\"frames\"]):\n",
" img = preprocess(fr, s[\"cond\"]) if use_pre else fr.astype(float)\n",
" lab, n = ndimage.label(img > img.mean() + k * img.std())\n",
" sizes = ndimage.sum(np.ones_like(img), lab, range(1, n + 1))\n",
" n_det = int(((sizes >= 3) & (sizes <= 60)).sum())\n",
" n_gt = sum(1 for l in s[\"labels\"] if l[\"frame\"] == t)\n",
" tp += min(n_det, n_gt); fp += max(0, n_det - n_gt); fn += max(0, n_gt - n_det)\n",
" p, r = tp / max(tp + fp, 1), tp / max(tp + fn, 1)\n",
" return 2 * p * r / max(p + r, 1e-9)\n",
"\n",
"\n",
"data = make_dataset(24, seed=7)\n",
"\n",
"# 1) 처리 속도 측정 (프레임/초)\n",
"frames = [(fr, s[\"cond\"]) for s in data for fr in s[\"frames\"]]\n",
"t0 = time.perf_counter()\n",
"for fr, c in frames:\n",
" normalize(preprocess(fr, c))\n",
"dt = time.perf_counter() - t0\n",
"print(f\"전처리 {len(frames)}프레임, {len(frames) / dt:,.0f} 프레임/초, 오류 0건\")\n",
"\n",
"# 2) 조건별 전처리 전후 탐지 F1 비교\n",
"print(\"\\n조건 전처리 전 F1 전처리 후 F1\")\n",
"for c in [\"clear\", \"night\", \"fog\", \"rain\"]:\n",
" sub = [s for s in data if s[\"cond\"] == c]\n",
" print(f\"{c:6s} {quick_f1(sub, False):10.3f} {quick_f1(sub, True):10.3f}\")\n",
"\n",
"# 3) 필터를 과하게 쓰면 소형 표적이 사라지는지 확인 (3x3 중간값 필터)\n",
"PARAMS[\"night\"][\"median\"] = 3\n",
"night = [s for s in data if s[\"cond\"] == \"night\"]\n",
"print(f\"\\n야간 3x3 중간값 필터 적용 시 F1: {quick_f1(night, True):.3f} (2px 소형 드론이 함께 지워짐)\")\n",
"PARAMS[\"night\"][\"median\"] = 1\n",
"\n",
"# 4) 결측 프레임 보정(앞 프레임으로 채우기)\n",
"seq = list(data[3][\"frames\"]); seq[5] = None; seq[6] = None\n",
"filled = list(seq)\n",
"for i in range(len(filled)):\n",
" if filled[i] is None:\n",
" filled[i] = filled[i - 1]\n",
"print(\"결측 프레임 2장 보정 완료:\", all(f is not None for f in filled))\n",
"\n",
"# 5) 전후 비교 그림\n",
"fig, ax = plt.subplots(2, 4, figsize=(12, 6))\n",
"for j, c in enumerate([\"clear\", \"night\", \"fog\", \"rain\"]):\n",
" s = next(x for x in data if x[\"cond\"] == c)\n",
" ax[0, j].imshow(s[\"frames\"][0], cmap=\"gray\"); ax[0, j].set_title(f\"{c} raw\")\n",
" ax[1, j].imshow(clahe_like(s[\"frames\"][0]) if c == \"fog\" else preprocess(s[\"frames\"][0], c), cmap=\"gray\")\n",
" ax[1, j].set_title(f\"{c} preprocessed\")\n",
"for a in ax.ravel():\n",
" a.axis(\"off\")\n",
"plt.suptitle(\"Synthetic IR frames: before / after preprocessing (training data)\")\n",
"plt.tight_layout()\n",
"plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<details>\n",
"<summary><b>💡 [참고] 미션 02 예상 실행 결과 (Output)</b></summary>\n",
"\n",
"```text\n",
"전처리 720프레임, 3,422 프레임/초, 오류 0건\n",
"\n",
"조건 전처리 전 F1 전처리 후 F1\n",
"clear 0.956 0.958\n",
"night 0.986 0.988\n",
"fog 0.989 0.991\n",
"rain 0.500 0.838\n",
"\n",
"야간 3x3 중간값 필터 적용 시 F1: 0.857 (2px 소형 드론이 함께 지워짐)\n",
"결측 프레임 2장 보정 완료: True\n",
"```\n",
"</details>\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## 🎯 미션 03. YOLO/규칙 기반 저고도 공중위협 객체 탐지 Baseline 프로토타입 구현\n",
"**소요시간**: 약 2.5시간\n",
"\n",
"### 📋 미션 배경 및 수행 지침\n",
"#### 미션 배경 및 당위성\n",
"\n",
"저고도 공중 비행 객체를 신속하게 1차 식별하기 위한 YOLO 경량 비전 모델 및 규칙 기반 바운딩 박스 추적 Baseline 프로토타입을 구현한다. 실제 전술 현장에서는 단순 탐지에 그치지 않고 신속한 위협 판단과 신뢰도 높은 의사결정이 필수적이므로, **저고도 공중 객체를 신속하게 1차 식별하는 규칙 기반 탐지 Baseline을 구현하고, 경량 YOLO를 쓸 경우와 장단점을 비교한다.** 과업을 완수하여 오판을 방지하고 작전 대응력을 극대화해야 합니다.\n",
"\n",
"이 문제를 해결하기 위해 본 미션에서는 **밝기 임계값(평균 + k x 표준편차)과 연결 영역 분석으로 바운딩 박스를 만들고 IoU 매칭으로 정밀도·재현율을 기상 조건별로 산출하고 모델에 적용하는** 실습을 체계적으로 수행합니다.\n",
"\n",
"학습자는 실습가이드에 제시된 총 4개 세부 활동(기본 기능 구현 ➔ 결과 지표 검증 ➔ 파라미터 튜닝 및 도전과제)을 순차적으로 진행하며 핵심 판정 기준을 달성합니다.\n",
"\n",
"**이번 결과물:** `src/models/ops01_baseline_model.py 및 기본 추론 결과 보고서` 형태의 결과물을 완성한다.\n",
"\n",
"### 🔍 평가 및 검증 기준 (PASS 조건)\n",
"#### 미션 판정\n",
"\n",
"**평가 구분:** 필수\n",
"\n",
"**핵심 평가영역:** 알고리즘의 논리적 흐름 및 군사적 판단 기준의 투명성\n",
"\n",
"#### 미션 PASS 조건\n",
"\n",
"- 정량 기준: 기본 데이터셋 대상 탐지/처리 정확도 75% 이상 달성\n",
"\n",
"\n",
"- 진행 기준: 단위 기능 동작 확인 및 기본 시나리오 실행 로그 생성\n",
"\n",
"\n",
"- 검증 방법: 기준 벤치마크 데이터셋 대상 추론 결과 오차율 비교\n",
"\n",
"\n",
"#### 채점자 판정 메모\n",
"\n",
"> 단순 코드 실행 여부뿐만 아니라 출력된 수치와 근거 자료가 군 실무 요구사항에 부합하는지 면밀히 확인한다.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"규칙 기반(밝기 임계값 + 연결 영역 분석) 저고도 공중위협 객체 탐지 Baseline을 구현하고, IoU 매칭으로 탐지 정확도(정밀도·재현율)를 측정한다. 경량 YOLO를 쓸 경우와의 차이도 함께 비교한다.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from matplotlib.patches import Rectangle\n",
"from scipy import ndimage\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [교육용가상자료] 합성 적외선 영상 생성기 (실제 영상·부대 정보 없음)\n",
"# 64x64 프레임에 드론·조류·민간기·지상 반사체(클러터)를 그리고 정답 라벨을 함께 만든다.\n",
"# ---------------------------------------------------------------------------\n",
"H = W = 64\n",
"PROFILE = {\"drone\": ((2, 3), (170, 225)), \"bird\": ((2, 3), (110, 150)),\n",
" \"civil\": ((4, 6), (190, 240)), \"clutter\": ((2, 4), (140, 190))}\n",
"\n",
"\n",
"def _new_obj(cls, rng, dive=False, v=None, xy=None):\n",
" # [TODO : 학생 실습 3] YOLO/규칙 기반 저고도 공중위협 객체 탐지 Baseline 프로토타입 구현 관련 핵심 로직을 구현하세요.\n",
" # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.\n",
" # <<< 여기에 코드를 작성하세요 >>>\n",
" pass # 작성 후 pass를 제거하세요.\n",
"def _noise(img, cond, rng):\n",
" # [TODO : 학생 실습 3] YOLO/규칙 기반 저고도 공중위협 객체 탐지 Baseline 프로토타입 구현 관련 핵심 로직을 구현하세요.\n",
" # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.\n",
" # <<< 여기에 코드를 작성하세요 >>>\n",
" pass # 작성 후 pass를 제거하세요.\n",
"def make_sequence(seq_id, cond, rng, kind=\"mixed\", n_frames=30, n_drone=None, n_bird=None):\n",
" \"\"\"시퀀스 1개 = (프레임 배열, 라벨 목록). kind: mixed / swarm / dive / saturation\"\"\"\n",
" objs = []\n",
" if kind == \"swarm\":\n",
" v, cx, cy = rng.uniform(-1, 1, 2), rng.uniform(15, 35), rng.uniform(15, 35)\n",
" objs = [_new_obj(\"drone\", rng, v=v, xy=(cx + i * 5, cy + (i % 2) * 5)) for i in range(4)]\n",
" objs.append(_new_obj(\"bird\", rng))\n",
" elif kind == \"dive\":\n",
" d = _new_obj(\"drone\", rng, dive=True)\n",
" d[\"y\"] = float(rng.uniform(4, 14))\n",
" objs = [d, _new_obj(\"bird\", rng), _new_obj(\"clutter\", rng)]\n",
" elif kind == \"saturation\":\n",
" objs = [_new_obj(\"drone\", rng) for _ in range(n_drone)] + [_new_obj(\"bird\", rng) for _ in range(n_bird)]\n",
" else:\n",
" objs = [_new_obj(\"drone\", rng) for _ in range(int(rng.integers(1, 3)))]\n",
" objs += [_new_obj(\"bird\", rng) for _ in range(int(rng.integers(0, 3)))]\n",
" if rng.random() < 0.5:\n",
" objs.append(_new_obj(\"civil\", rng))\n",
" if rng.random() < 0.6:\n",
" objs.append(_new_obj(\"clutter\", rng))\n",
" frames, labels = np.zeros((n_frames, H, W), np.uint8), []\n",
" base = rng.normal(40, 6, (H, W))\n",
" for t in range(n_frames):\n",
" canvas = base.copy()\n",
" for i, o in enumerate(objs):\n",
" if t > 0:\n",
" if o[\"cls\"] == \"bird\":\n",
" o[\"vx\"] = float(np.clip(o[\"vx\"] + rng.normal(0, 0.45), -1.6, 1.6))\n",
" o[\"vy\"] = float(np.clip(o[\"vy\"] + rng.normal(0, 0.45), -1.6, 1.6))\n",
" if o[\"dive\"] and t > 10:\n",
" o[\"vy\"] += 0.18\n",
" o[\"x\"] = float(np.clip(o[\"x\"] + o[\"vx\"], 1, W - o[\"size\"] - 1))\n",
" o[\"y\"] = float(np.clip(o[\"y\"] + o[\"vy\"], 1, H - o[\"size\"] - 1))\n",
" heat = o[\"heat\"]\n",
" if o[\"cls\"] == \"clutter\" and rng.random() < 0.3:\n",
" heat *= 0.7\n",
" if o[\"cls\"] == \"bird\":\n",
" heat *= float(rng.uniform(0.85, 1.1))\n",
" x, y, s = int(round(o[\"x\"])), int(round(o[\"y\"])), o[\"size\"]\n",
" canvas[y:y + s, x:x + s] = np.maximum(canvas[y:y + s, x:x + s], heat)\n",
" labels.append({\"frame\": t, \"track\": f\"{seq_id}_T{i:02d}\", \"class\": o[\"cls\"], \"bbox\": [x, y, s, s],\n",
" \"pattern\": \"dive\" if o[\"dive\"] else (\"swarm\" if kind == \"swarm\" and o[\"cls\"] == \"drone\" else \"normal\")})\n",
" frames[t] = _noise(canvas, cond, rng)\n",
" return frames, labels\n",
"\n",
"\n",
"def make_dataset(n=24, seed=7):\n",
" rng = np.random.default_rng(seed)\n",
" conds = [\"clear\", \"night\", \"fog\", \"rain\"]\n",
" out = []\n",
" for i in range(n):\n",
" kind = \"swarm\" if i % 8 == 5 else (\"dive\" if i % 8 == 6 else \"mixed\")\n",
" f, lab = make_sequence(f\"SEQ{i:03d}\", conds[i % 4], rng, kind)\n",
" out.append({\"id\": f\"SEQ{i:03d}\", \"cond\": conds[i % 4], \"kind\": kind, \"frames\": f, \"labels\": lab})\n",
" return out\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- 전처리·탐지 공통 함수 ----\n",
"def preprocess(frame, cond):\n",
" f = frame.astype(float)\n",
" if cond == \"rain\": # 폭 1px 세로 빗줄기 제거(가로 방향 형태학적 열림)\n",
" f = ndimage.grey_opening(f, size=(1, 2))\n",
" lo, hi = np.percentile(f, [1, 99.5]) # 대비 확장\n",
" return np.clip((f - lo) / (hi - lo + 1e-6) * 255, 0, 255)\n",
"\n",
"\n",
"def detect(frame, cond, k=2.0, min_area=3, max_area=60):\n",
" img = preprocess(frame, cond)\n",
" lab, _ = ndimage.label(img > img.mean() + k * img.std())\n",
" dets = []\n",
" for i, sl in enumerate(ndimage.find_objects(lab), start=1):\n",
" area = int((lab[sl] == i).sum())\n",
" if min_area <= area <= max_area:\n",
" y, x = sl[0].start, sl[1].start\n",
" dets.append({\"bbox\": [x, y, sl[1].stop - x, sl[0].stop - y], \"area\": area,\n",
" \"thermal\": float(frame[sl][lab[sl] == i].max())})\n",
" return dets\n",
"\n",
"\n",
"def iou(a, b):\n",
" ix = max(0, min(a[0] + a[2], b[0] + b[2]) - max(a[0], b[0]))\n",
" iy = max(0, min(a[1] + a[3], b[1] + b[3]) - max(a[1], b[1]))\n",
" inter = ix * iy\n",
" return inter / (a[2] * a[3] + b[2] * b[3] - inter)\n",
"\n",
"\n",
"def evaluate(data, k=2.0, classes=(\"drone\", \"bird\", \"civil\", \"clutter\")):\n",
" tp = fp = fn = 0\n",
" for s in data:\n",
" for t, fr in enumerate(s[\"frames\"]):\n",
" gts = [l[\"bbox\"] for l in s[\"labels\"] if l[\"frame\"] == t and l[\"class\"] in classes]\n",
" used = set()\n",
" dets = detect(fr, s[\"cond\"], k)\n",
" for d in dets:\n",
" best = max(((iou(d[\"bbox\"], g), j) for j, g in enumerate(gts) if j not in used), default=(0, -1))\n",
" if best[0] >= 0.1:\n",
" tp += 1; used.add(best[1])\n",
" fp += len(dets) - len(used); fn += len(gts) - len(used)\n",
" p, r = tp / max(tp + fp, 1), tp / max(tp + fn, 1)\n",
" return {\"TP\": tp, \"FP\": fp, \"FN\": fn, \"정밀도\": round(p, 3), \"재현율\": round(r, 3), \"F1\": round(2 * p * r / max(p + r, 1e-9), 3)}\n",
"\n",
"\n",
"data = make_dataset(24, seed=7)\n",
"\n",
"# 1) 기상 조건별 객체 탐지 성능 (드론·조류·민간기·클러터 모두 '객체'로 봄)\n",
"rows = []\n",
"for c in [\"clear\", \"night\", \"fog\", \"rain\"]:\n",
" rows.append({\"조건\": c, **evaluate([s for s in data if s[\"cond\"] == c])})\n",
"rows.append({\"조건\": \"전체\", **evaluate(data)})\n",
"print(pd.DataFrame(rows).to_string(index=False))\n",
"acc = rows[-1][\"재현율\"]\n",
"print(f\"\\n목표(탐지 정확도 75% 이상) 달성 여부: {'달성' if acc >= 0.75 else '미달'} (재현율 {acc:.1%})\")\n",
"\n",
"# 2) 드론만 기준으로 보면? -> 조류·민간기까지 잡혀 정밀도가 크게 떨어진다 (미션 4·5의 동기)\n",
"d = evaluate(data, classes=(\"drone\",))\n",
"print(f\"드론만 정답으로 볼 때 정밀도 {d['정밀도']:.1%}, 재현율 {d['재현율']:.1%}\")\n",
"\n",
"# 3) 임계값 k에 따른 민감도 변화\n",
"print(\"\\nk 정밀도 재현율\")\n",
"for k in [1.5, 2.0, 2.5, 3.0, 3.5]:\n",
" e = evaluate(data, k)\n",
" print(f\"{k:<4} {e['정밀도']:.3f} {e['재현율']:.3f}\")\n",
"\n",
"# 4) 규칙 기반 vs 경량 YOLO 비교표\n",
"print(pd.DataFrame({\"항목\": [\"학습 데이터\", \"연산 자원\", \"소형 표적(2~3px)\", \"판단 근거 설명\", \"도입 속도\"],\n",
" \"규칙 기반(본 Baseline)\": [\"불필요\", \"CPU로 충분\", \"밝기 차이에 의존\", \"쉬움(임계값)\", \"즉시\"],\n",
" \"경량 YOLO\": [\"라벨 수천 장 이상\", \"GPU 권장\", \"학습 데이터 품질에 의존\", \"어려움\", \"학습 후\"]}).to_string(index=False))\n",
"\n",
"# 5) 탐지 결과 시각화\n",
"s = data[5]\n",
"fig, ax = plt.subplots(1, 3, figsize=(12, 4))\n",
"for a, t in zip(ax, [0, 10, 20]):\n",
" a.imshow(s[\"frames\"][t], cmap=\"gray\")\n",
" for det in detect(s[\"frames\"][t], s[\"cond\"]):\n",
" x, y, w, h = det[\"bbox\"]\n",
" a.add_patch(Rectangle((x - .5, y - .5), w, h, fill=False, color=\"red\"))\n",
" a.set_title(f\"{s['id']} frame {t} (red = detection)\")\n",
" a.axis(\"off\")\n",
"plt.tight_layout()\n",
"plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<details>\n",
"<summary><b>💡 [참고] 미션 03 예상 실행 결과 (Output)</b></summary>\n",
"\n",
"```text\n",
" 조건 TP FP FN 정밀도 재현율 F1\n",
"clear 767 0 43 1.000 0.947 0.973\n",
"night 799 10 11 0.988 0.986 0.987\n",
" fog 561 28 9 0.952 0.984 0.968\n",
" rain 591 391 9 0.602 0.985 0.747\n",
" 전체 2718 429 72 0.864 0.974 0.916\n",
"\n",
"목표(탐지 정확도 75% 이상) 달성 여부: 달성 (재현율 97.4%)\n",
"드론만 정답으로 볼 때 정밀도 39.8%, 재현율 99.4%\n",
"\n",
"k 정밀도 재현율\n",
"1.5 0.569 0.976\n",
"2.0 0.864 0.974\n",
"2.5 0.920 0.963\n",
"3.0 0.933 0.878\n",
"3.5 0.951 0.764\n",
" 항목 규칙 기반(본 Baseline) 경량 YOLO\n",
" 학습 데이터 불필요 라벨 수천 장 이상\n",
" 연산 자원 CPU로 충분 GPU 권장\n",
"소형 표적(2~3px) 밝기 차이에 의존 학습 데이터 품질에 의존\n",
" 판단 근거 설명 쉬움(임계값) 어려움\n",
" 도입 속도 즉시 학습 후\n",
"```\n",
"</details>\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## 🎯 미션 04. 군집 드론 및 비행체 기만 비행 패턴 식별 복합 위협 탐지 모델 고도화\n",
"**소요시간**: 약 3.0시간\n",
"\n",
"### 📋 미션 배경 및 수행 지침\n",
"#### 미션 배경 및 당위성\n",
"\n",
"적 군집 드론(Swarm)의 분산 비행, 급강하 자폭 궤적 및 비행체 기만 비행 패턴을 식별하기 위한 다중 객체 딥러닝 탐지 모델을 고도화한다. 실제 전술 현장에서는 단순 탐지에 그치지 않고 신속한 위협 판단과 신뢰도 높은 의사결정이 필수적이므로, **군집 드론의 동행 비행, 급강하 자폭 궤적, 조류처럼 흔들리는 기만 비행 패턴을 식별하도록 탐지를 궤적 단위로 고도화한다.** 과업을 완수하여 오판을 방지하고 작전 대응력을 극대화해야 합니다.\n",
"\n",
"이 문제를 해결하기 위해 본 미션에서는 **예측 위치 기반 최근접 이웃 추적기로 프레임별 탐지를 궤적으로 잇고, 속도·방향 전환·직진성·수직 가속·크기·열신호 특징을 계산해 정속 직진·급강하·불규칙·군집 동행 패턴 판정하고 정확도를 측정하는** 실습을 체계적으로 수행합니다.\n",
"\n",
"학습자는 실습가이드에 제시된 총 4개 세부 활동(기본 기능 구현 ➔ 결과 지표 검증 ➔ 파라미터 튜닝 및 도전과제)을 순차적으로 진행하며 핵심 판정 기준을 달성합니다.\n",
"\n",
"**이번 결과물:** `src/models/ops01_advanced_detector.py 및 예외 탐지 테스트 결과` 형태의 결과물을 완성한다.\n",
"\n",
"### 🔍 평가 및 검증 기준 (PASS 조건)\n",
"#### 미션 판정\n",
"\n",
"**평가 구분:** 필수\n",
"\n",
"**핵심 평가영역:** 기만/교란 공격에 대한 방어 로직의 현실성 및 신뢰성\n",
"\n",
"#### 미션 PASS 조건\n",
"\n",
"- 정량 기준: 복합 위협 탐지 재현율(Recall) 85% 이상, 처리 지연 200ms 이하\n",
"\n",
"\n",
"- 진행 기준: 복합 위협 시나리오 10종에 대한 탐지 테스트 통과\n",
"\n",
"\n",
"- 검증 방법: 합성 위협 데이터셋 주입 시 이상치 탐지 정확도 검증\n",
"\n",
"\n",
"#### 채점자 판정 메모\n",
"\n",
"> 단순 코드 실행 여부뿐만 아니라 출력된 수치와 근거 자료가 군 실무 요구사항에 부합하는지 면밀히 확인한다.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"프레임별 탐지를 궤적으로 연결(추적)하고, 궤적의 움직임 특징으로 군집 드론(동일 속도 동행)·급강하 자폭 궤적·불규칙(조류형 기만) 비행 패턴을 식별해 복합 위협 탐지 재현율과 처리 지연을 측정한다.\"\"\"\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy import ndimage\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [교육용가상자료] 합성 적외선 영상 생성기 (실제 영상·부대 정보 없음)\n",
"# 64x64 프레임에 드론·조류·민간기·지상 반사체(클러터)를 그리고 정답 라벨을 함께 만든다.\n",
"# ---------------------------------------------------------------------------\n",
"H = W = 64\n",
"PROFILE = {\"drone\": ((2, 3), (170, 225)), \"bird\": ((2, 3), (110, 150)),\n",
" \"civil\": ((4, 6), (190, 240)), \"clutter\": ((2, 4), (140, 190))}\n",
"\n",
"\n",
"def _new_obj(cls, rng, dive=False, v=None, xy=None):\n",
" # [TODO : 학생 실습 4] 군집 드론 및 비행체 기만 비행 패턴 식별 복합 위협 탐지 모델 고도화 관련 핵심 로직을 구현하세요.\n",
" # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.\n",
" # <<< 여기에 코드를 작성하세요 >>>\n",
" pass # 작성 후 pass를 제거하세요.\n",
"def _noise(img, cond, rng):\n",
" # [TODO : 학생 실습 4] 군집 드론 및 비행체 기만 비행 패턴 식별 복합 위협 탐지 모델 고도화 관련 핵심 로직을 구현하세요.\n",
" # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.\n",
" # <<< 여기에 코드를 작성하세요 >>>\n",
" pass # 작성 후 pass를 제거하세요.\n",
"def make_sequence(seq_id, cond, rng, kind=\"mixed\", n_frames=30, n_drone=None, n_bird=None):\n",
" \"\"\"시퀀스 1개 = (프레임 배열, 라벨 목록). kind: mixed / swarm / dive / saturation\"\"\"\n",
" objs = []\n",
" if kind == \"swarm\":\n",
" v, cx, cy = rng.uniform(-1, 1, 2), rng.uniform(15, 35), rng.uniform(15, 35)\n",
" objs = [_new_obj(\"drone\", rng, v=v, xy=(cx + i * 5, cy + (i % 2) * 5)) for i in range(4)]\n",
" objs.append(_new_obj(\"bird\", rng))\n",
" elif kind == \"dive\":\n",
" d = _new_obj(\"drone\", rng, dive=True)\n",
" d[\"y\"] = float(rng.uniform(4, 14))\n",
" objs = [d, _new_obj(\"bird\", rng), _new_obj(\"clutter\", rng)]\n",
" elif kind == \"saturation\":\n",
" objs = [_new_obj(\"drone\", rng) for _ in range(n_drone)] + [_new_obj(\"bird\", rng) for _ in range(n_bird)]\n",
" else:\n",
" objs = [_new_obj(\"drone\", rng) for _ in range(int(rng.integers(1, 3)))]\n",
" objs += [_new_obj(\"bird\", rng) for _ in range(int(rng.integers(0, 3)))]\n",
" if rng.random() < 0.5:\n",
" objs.append(_new_obj(\"civil\", rng))\n",
" if rng.random() < 0.6:\n",
" objs.append(_new_obj(\"clutter\", rng))\n",
" frames, labels = np.zeros((n_frames, H, W), np.uint8), []\n",
" base = rng.normal(40, 6, (H, W))\n",
" for t in range(n_frames):\n",
" canvas = base.copy()\n",
" for i, o in enumerate(objs):\n",
" if t > 0:\n",
" if o[\"cls\"] == \"bird\":\n",
" o[\"vx\"] = float(np.clip(o[\"vx\"] + rng.normal(0, 0.45), -1.6, 1.6))\n",
" o[\"vy\"] = float(np.clip(o[\"vy\"] + rng.normal(0, 0.45), -1.6, 1.6))\n",
" if o[\"dive\"] and t > 10:\n",
" o[\"vy\"] += 0.18\n",
" o[\"x\"] = float(np.clip(o[\"x\"] + o[\"vx\"], 1, W - o[\"size\"] - 1))\n",
" o[\"y\"] = float(np.clip(o[\"y\"] + o[\"vy\"], 1, H - o[\"size\"] - 1))\n",
" heat = o[\"heat\"]\n",
" if o[\"cls\"] == \"clutter\" and rng.random() < 0.3:\n",
" heat *= 0.7\n",
" if o[\"cls\"] == \"bird\":\n",
" heat *= float(rng.uniform(0.85, 1.1))\n",
" x, y, s = int(round(o[\"x\"])), int(round(o[\"y\"])), o[\"size\"]\n",
" canvas[y:y + s, x:x + s] = np.maximum(canvas[y:y + s, x:x + s], heat)\n",
" labels.append({\"frame\": t, \"track\": f\"{seq_id}_T{i:02d}\", \"class\": o[\"cls\"], \"bbox\": [x, y, s, s],\n",
" \"pattern\": \"dive\" if o[\"dive\"] else (\"swarm\" if kind == \"swarm\" and o[\"cls\"] == \"drone\" else \"normal\")})\n",
" frames[t] = _noise(canvas, cond, rng)\n",
" return frames, labels\n",
"\n",
"\n",
"def make_dataset(n=24, seed=7):\n",
" rng = np.random.default_rng(seed)\n",
" conds = [\"clear\", \"night\", \"fog\", \"rain\"]\n",
" out = []\n",
" for i in range(n):\n",
" kind = \"swarm\" if i % 8 == 5 else (\"dive\" if i % 8 == 6 else \"mixed\")\n",
" f, lab = make_sequence(f\"SEQ{i:03d}\", conds[i % 4], rng, kind)\n",
" out.append({\"id\": f\"SEQ{i:03d}\", \"cond\": conds[i % 4], \"kind\": kind, \"frames\": f, \"labels\": lab})\n",
" return out\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- 전처리·탐지 공통 함수 ----\n",
"def preprocess(frame, cond):\n",
" f = frame.astype(float)\n",
" if cond == \"rain\": # 폭 1px 세로 빗줄기 제거(가로 방향 형태학적 열림)\n",
" f = ndimage.grey_opening(f, size=(1, 2))\n",
" lo, hi = np.percentile(f, [1, 99.5]) # 대비 확장\n",
" return np.clip((f - lo) / (hi - lo + 1e-6) * 255, 0, 255)\n",
"\n",
"\n",
"def detect(frame, cond, k=2.0, min_area=3, max_area=60):\n",
" img = preprocess(frame, cond)\n",
" lab, _ = ndimage.label(img > img.mean() + k * img.std())\n",
" dets = []\n",
" for i, sl in enumerate(ndimage.find_objects(lab), start=1):\n",
" area = int((lab[sl] == i).sum())\n",
" if min_area <= area <= max_area:\n",
" y, x = sl[0].start, sl[1].start\n",
" dets.append({\"bbox\": [x, y, sl[1].stop - x, sl[0].stop - y], \"area\": area,\n",
" \"thermal\": float(frame[sl][lab[sl] == i].max())})\n",
" return dets\n",
"\n",
"\n",
"def iou(a, b):\n",
" ix = max(0, min(a[0] + a[2], b[0] + b[2]) - max(a[0], b[0]))\n",
" iy = max(0, min(a[1] + a[3], b[1] + b[3]) - max(a[1], b[1]))\n",
" inter = ix * iy\n",
" return inter / (a[2] * a[3] + b[2] * b[3] - inter)\n",
"\n",
"# ---- 추적·특징·패턴 공통 함수 ----\n",
"def center(b):\n",
" return np.array([b[0] + b[2] / 2, b[1] + b[3] / 2])\n",
"\n",
"\n",
"def track(per_frame, gate=6.0, max_missed=2, min_len=5):\n",
" \"\"\"예측 위치(마지막 위치 + 속도)와 가장 가까운 탐지를 이어 궤적을 만든다.\"\"\"\n",
" active, done = [], []\n",
" for t, dets in enumerate(per_frame):\n",
" used = set()\n",
" for tr in sorted(active, key=lambda x: -len(x[\"t\"])):\n",
" last = center(tr[\"b\"][-1])\n",
" vel = last - center(tr[\"b\"][-2]) if len(tr[\"b\"]) > 1 else np.zeros(2)\n",
" pred = last + vel * (t - tr[\"t\"][-1])\n",
" cand = [(np.linalg.norm(center(d[\"bbox\"]) - pred), j) for j, d in enumerate(dets) if j not in used]\n",
" dist, j = min(cand, default=(gate, -1))\n",
" if j >= 0 and dist < gate:\n",
" used.add(j)\n",
" tr[\"t\"].append(t); tr[\"b\"].append(dets[j][\"bbox\"])\n",
" tr[\"area\"].append(dets[j][\"area\"]); tr[\"heat\"].append(dets[j][\"thermal\"])\n",
" still = []\n",
" for tr in active:\n",
" (still if t - tr[\"t\"][-1] <= max_missed else done).append(tr)\n",
" active = still\n",
" for j, d in enumerate(dets):\n",
" if j not in used:\n",
" active.append({\"t\": [t], \"b\": [d[\"bbox\"]], \"area\": [d[\"area\"]], \"heat\": [d[\"thermal\"]]})\n",
" return [tr for tr in done + active if len(tr[\"t\"]) >= min_len]\n",
"\n",
"\n",
"FEATURES = [\"mean_speed\", \"speed_std\", \"turn\", \"straight\", \"vy_accel\", \"area\", \"heat\", \"heat_std\"]\n",
"\n",
"\n",
"def features(tr):\n",
" c = np.array([center(b) for b in tr[\"b\"]]); f = np.array(tr[\"t\"], float)\n",
" v = np.diff(c, axis=0) / np.maximum(np.diff(f), 1)[:, None]\n",
" sp = np.linalg.norm(v, axis=1)\n",
" k = 3 if len(c) > 4 else 1\n",
" v3 = (c[k:] - c[:-k]) / np.maximum(f[k:] - f[:-k], 1)[:, None]\n",
" hd = np.arctan2(v3[:, 1], v3[:, 0]); mv = np.linalg.norm(v3, axis=1) > 0.2\n",
" turns = np.abs(np.angle(np.exp(1j * np.diff(hd))))[mv[1:] & mv[:-1]]\n",
" half = len(v) // 2\n",
" return {\"mean_speed\": sp.mean(), \"speed_std\": sp.std(), \"turn\": turns.mean() if len(turns) else 0.0,\n",
" \"straight\": np.linalg.norm(c[-1] - c[0]) / max(sp.sum(), 1e-6),\n",
" \"vy_accel\": (v[half:, 1].mean() - v[:half, 1].mean()) / max(1, (f[-1] - f[0]) / 2) if half else 0.0,\n",
" \"area\": np.mean(tr[\"area\"]), \"heat\": np.mean(tr[\"heat\"]), \"heat_std\": np.std(tr[\"heat\"]),\n",
" \"vx\": v[:, 0].mean(), \"vy\": v[:, 1].mean(), \"x\": c[-1, 0], \"y\": c[-1, 1]}\n",
"\n",
"\n",
"def pattern(f):\n",
" if f[\"mean_speed\"] < 0.25:\n",
" return \"static\"\n",
" if f[\"vy_accel\"] > 0.06:\n",
" return \"dive\"\n",
" if f[\"turn\"] > 0.4 or f[\"straight\"] < 0.6:\n",
" return \"erratic\"\n",
" return \"steady\"\n",
"\n",
"\n",
"def swarms(trs, vel=0.2, dist=18.0, min_n=3):\n",
" \"\"\"속도가 비슷하고 가까이 움직이는 궤적 3개 이상을 군집으로 묶는다.\"\"\"\n",
" cand = [i for i, t in enumerate(trs) if t[\"pattern\"] != \"static\"]\n",
" adj = {i: set() for i in cand}\n",
" for a in cand:\n",
" for b in cand:\n",
" fa, fb = trs[a][\"f\"], trs[b][\"f\"]\n",
" if a < b and np.hypot(fa[\"vx\"] - fb[\"vx\"], fa[\"vy\"] - fb[\"vy\"]) < vel and np.hypot(fa[\"x\"] - fb[\"x\"], fa[\"y\"] - fb[\"y\"]) < dist:\n",
" adj[a].add(b); adj[b].add(a)\n",
" seen, groups = set(), []\n",
" for i in cand:\n",
" if i in seen:\n",
" continue\n",
" stack, comp = [i], []\n",
" while stack:\n",
" k = stack.pop()\n",
" if k not in seen:\n",
" seen.add(k); comp.append(k); stack += list(adj[k] - seen)\n",
" if len(comp) >= min_n:\n",
" groups.append(comp)\n",
" return groups\n",
"\n",
"\n",
"def analyze(s, k=2.0):\n",
" \"\"\"시퀀스 1개: 탐지 -> 추적 -> 특징 -> 패턴 -> 군집 -> 정답 라벨 연결(평가용).\"\"\"\n",
" per = [detect(fr, s[\"cond\"], k) if fr is not None else [] for fr in s[\"frames\"]]\n",
" trs = track(per)\n",
" for tr in trs:\n",
" tr[\"f\"] = features(tr); tr[\"pattern\"] = pattern(tr[\"f\"]); tr[\"swarm\"] = None; tr[\"seq\"] = s[\"id\"]\n",
" votes = {}\n",
" for t, b in zip(tr[\"t\"], tr[\"b\"]):\n",
" best = max(((iou(b, l[\"bbox\"]), l) for l in s[\"labels\"] if l[\"frame\"] == t), default=(0, None), key=lambda x: x[0])\n",
" key = (best[1][\"track\"], best[1][\"class\"], best[1][\"pattern\"]) if best[0] > 0.05 else (\"none\", \"noise\", \"normal\")\n",
" votes[key] = votes.get(key, 0) + 1\n",
" tr[\"gt_track\"], tr[\"gt_class\"], tr[\"gt_pattern\"] = max(votes, key=votes.get)\n",
" for g, comp in enumerate(swarms(trs)):\n",
" for i in comp:\n",
" trs[i][\"swarm\"] = g\n",
" if trs[i][\"pattern\"] in (\"steady\", \"erratic\"):\n",
" trs[i][\"pattern\"] = \"swarm\"\n",
" return trs\n",
"\n",
"\n",
"data = make_dataset(24, seed=7)\n",
"t0 = time.perf_counter()\n",
"tracks = [tr for s in data for tr in analyze(s)]\n",
"latency = (time.perf_counter() - t0) / sum(len(s[\"frames\"]) for s in data) * 1000\n",
"\n",
"# 1) 드론 궤적 재현율: 정답 드론 중 '움직이는 위협 후보'로 잡힌 비율\n",
"gt_drones = {l[\"track\"] for s in data for l in s[\"labels\"] if l[\"class\"] == \"drone\"}\n",
"found = {t[\"gt_track\"] for t in tracks if t[\"gt_class\"] == \"drone\" and t[\"pattern\"] != \"static\"}\n",
"recall = len(found) / len(gt_drones)\n",
"print(f\"드론 궤적 재현율 {recall:.1%} (목표 85% 이상: {'달성' if recall >= 0.85 else '미달'}), 프레임당 처리 {latency:.2f} ms (목표 200 ms 이하)\")\n",
"\n",
"# 2) 실제 정체 x 판정 패턴 교차표\n",
"print(\"\\n[실제 정체별 판정 패턴]\")\n",
"print(pd.crosstab(pd.Series([t[\"gt_class\"] for t in tracks], name=\"실제\"), pd.Series([t[\"pattern\"] for t in tracks], name=\"판정\")))\n",
"\n",
"# 3) 급강하·군집 시나리오 식별\n",
"dives = [t for t in tracks if t[\"gt_pattern\"] == \"dive\"]\n",
"print(f\"\\n급강하 드론 {len(dives)}개 중 dive 판정 {sum(t['pattern'] == 'dive' for t in dives)}개\")\n",
"for sid in sorted({t[\"seq\"] for t in tracks if t[\"gt_pattern\"] == \"swarm\"}):\n",
" n = sum(1 for t in tracks if t[\"seq\"] == sid and t[\"swarm\"] is not None and t[\"gt_class\"] == \"drone\")\n",
" print(f\"군집 시나리오 {sid}: 군집으로 묶인 드론 궤적 {n}개\")\n",
"print(\"\\n참고: 화면 가장자리에 닿아 속도가 왜곡되면 군집이 끊길 수 있다. 조류처럼 흔들리며 나는 기만 비행은 움직임만으로 구분이 어려워 미션 5에서 열신호 등과 함께 판단한다.\")\n",
"\n",
"# 4) 궤적 시각화\n",
"s = next(x for x in data if x[\"kind\"] == \"swarm\")\n",
"trs = analyze(s)\n",
"color = {\"steady\": \"tab:blue\", \"dive\": \"tab:red\", \"erratic\": \"tab:gray\", \"swarm\": \"tab:purple\", \"static\": \"black\"}\n",
"plt.figure(figsize=(6, 6))\n",
"plt.imshow(s[\"frames\"][-1], cmap=\"gray\")\n",
"for t in trs:\n",
" c = np.array([center(b) for b in t[\"b\"]])\n",
" plt.plot(c[:, 0], c[:, 1], \"-\", color=color[t[\"pattern\"]], lw=2, label=t[\"pattern\"])\n",
"h, l = plt.gca().get_legend_handles_labels()\n",
"plt.legend(dict(zip(l, h)).values(), dict(zip(l, h)).keys())\n",
"plt.title(f\"{s['id']} tracks by pattern (synthetic)\")\n",
"plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<details>\n",
"<summary><b>💡 [참고] 미션 04 예상 실행 결과 (Output)</b></summary>\n",
"\n",
"```text\n",
"드론 궤적 재현율 100.0% (목표 85% 이상: 달성), 프레임당 처리 1.02 ms (목표 200 ms 이하)\n",
"\n",
"[실제 정체별 판정 패턴]\n",
"판정 dive erratic static steady swarm\n",
"실제 \n",
"bird 5 12 0 12 0\n",
"civil 0 5 0 4 0\n",
"clutter 0 4 11 0 0\n",
"drone 3 4 0 25 12\n",
"\n",
"급강하 드론 3개 중 dive 판정 3개\n",
"군집 시나리오 SEQ005: 군집으로 묶인 드론 궤적 4개\n",
"군집 시나리오 SEQ013: 군집으로 묶인 드론 궤적 4개\n",
"군집 시나리오 SEQ021: 군집으로 묶인 드론 궤적 4개\n",
"\n",
"참고: 화면 가장자리에 닿아 속도가 왜곡되면 군집이 끊길 수 있다. 조류처럼 흔들리며 나는 기만 비행은 움직임만으로 구분이 어려워 미션 5에서 열신호 등과 함께 판단한다.\n",
"```\n",
"</details>\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## 🎯 미션 05. 조류·민간기 등 오표적 식별 방지 및 신뢰도 점수(Confidence Score) 산출 엔진 구현\n",
"**소요시간**: 약 3.0시간\n",
"\n",
"### 📋 미션 배경 및 수행 지침\n",
"#### 미션 배경 및 당위성\n",
"\n",
"조류, 민간 비행체, 레이더 반사체 등 오표적(False Alarm)을 식별하고 비행체 크기·속도·열 신호 기반 다차원 신뢰도 점수(Confidence Score) 산출 엔진을 개발한다. 실제 전술 현장에서는 단순 탐지에 그치지 않고 신속한 위협 판단과 신뢰도 높은 의사결정이 필수적이므로, **조류·민간 비행체·반사체 등 오표적을 걸러내기 위해 크기·속도·열신호 기반 신뢰도 점수(Confidence Score)와 등급을 산출하는 엔진을 개발한다.** 과업을 완수하여 오판을 방지하고 작전 대응력을 극대화해야 합니다.\n",
"\n",
"이 문제를 해결하기 위해 본 미션에서는 **학습용과 검증용 가상 데이터를 분리해 로지스틱 회귀 기반 엔진 학습하여 등급별 신뢰도를 판정하는** 실습을 체계적으로 수행합니다.\n",
"\n",
"학습자는 실습가이드에 제시된 총 4개 세부 활동(기본 기능 구현 ➔ 결과 지표 검증 ➔ 파라미터 튜닝 및 도전과제)을 순차적으로 진행하며 핵심 판정 기준을 달성합니다.\n",
"\n",
"**이번 결과물:** `src/verification/ops01_confidence_engine.py` 형태의 결과물을 완성한다.\n",
"\n",
"### 🔍 평가 및 검증 기준 (PASS 조건)\n",
"#### 미션 판정\n",
"\n",
"**평가 구분:** 필수\n",
"\n",
"**핵심 평가영역:** 신뢰도 등급(High/Med/Low) 부여 기준의 군사적 타당성\n",
"\n",
"#### 미션 PASS 조건\n",
"\n",
"- 정량 기준: 오경보율(FAR) 40% 이상 감축, 신뢰도 스코어 산출 일관성 90% 이상\n",
"\n",
"\n",
"- 진행 기준: 오경보 필터링 전후 비교 지표 산출 및 통합 테스트 완료\n",
"\n",
"\n",
"- 검증 방법: ROC-AUC 곡선 분석 및 임계값 민감도 테스트\n",
"\n",
"\n",
"#### 채점자 판정 메모\n",
"\n",
"> 단순 코드 실행 여부뿐만 아니라 출력된 수치와 근거 자료가 군 실무 요구사항에 부합하는지 면밀히 확인한다.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"조류·민간기·지상 반사체 등 오표적을 걸러내기 위해 궤적의 크기·속도·열신호 특징으로 '드론일 확률'(Confidence Score)과 등급(High/Medium/Low)을 계산하고, 기존 방식 대비 오경보율(FAR) 감축과 점수 일관성을 검증한다.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy import ndimage\n",
"from sklearn.linear_model import LogisticRegression\n",
"from sklearn.pipeline import make_pipeline\n",
"from sklearn.preprocessing import StandardScaler\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [교육용가상자료] 합성 적외선 영상 생성기 (실제 영상·부대 정보 없음)\n",
"# 64x64 프레임에 드론·조류·민간기·지상 반사체(클러터)를 그리고 정답 라벨을 함께 만든다.\n",
"# ---------------------------------------------------------------------------\n",
"H = W = 64\n",
"PROFILE = {\"drone\": ((2, 3), (170, 225)), \"bird\": ((2, 3), (110, 150)),\n",
" \"civil\": ((4, 6), (190, 240)), \"clutter\": ((2, 4), (140, 190))}\n",
"\n",
"\n",
"def _new_obj(cls, rng, dive=False, v=None, xy=None):\n",
" # [TODO : 학생 실습 5] 조류·민간기 등 오표적 식별 방지 및 신뢰도 점수(Confidence Score) 산출 엔진 구현 관련 핵심 로직을 구현하세요.\n",
" # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.\n",
" # <<< 여기에 코드를 작성하세요 >>>\n",
" pass # 작성 후 pass를 제거하세요.\n",
"def _noise(img, cond, rng):\n",
" # [TODO : 학생 실습 5] 조류·민간기 등 오표적 식별 방지 및 신뢰도 점수(Confidence Score) 산출 엔진 구현 관련 핵심 로직을 구현하세요.\n",
" # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.\n",
" # <<< 여기에 코드를 작성하세요 >>>\n",
" pass # 작성 후 pass를 제거하세요.\n",
"def make_sequence(seq_id, cond, rng, kind=\"mixed\", n_frames=30, n_drone=None, n_bird=None):\n",
" \"\"\"시퀀스 1개 = (프레임 배열, 라벨 목록). kind: mixed / swarm / dive / saturation\"\"\"\n",
" objs = []\n",
" if kind == \"swarm\":\n",
" v, cx, cy = rng.uniform(-1, 1, 2), rng.uniform(15, 35), rng.uniform(15, 35)\n",
" objs = [_new_obj(\"drone\", rng, v=v, xy=(cx + i * 5, cy + (i % 2) * 5)) for i in range(4)]\n",
" objs.append(_new_obj(\"bird\", rng))\n",
" elif kind == \"dive\":\n",
" d = _new_obj(\"drone\", rng, dive=True)\n",
" d[\"y\"] = float(rng.uniform(4, 14))\n",
" objs = [d, _new_obj(\"bird\", rng), _new_obj(\"clutter\", rng)]\n",
" elif kind == \"saturation\":\n",
" objs = [_new_obj(\"drone\", rng) for _ in range(n_drone)] + [_new_obj(\"bird\", rng) for _ in range(n_bird)]\n",
" else:\n",
" objs = [_new_obj(\"drone\", rng) for _ in range(int(rng.integers(1, 3)))]\n",
" objs += [_new_obj(\"bird\", rng) for _ in range(int(rng.integers(0, 3)))]\n",
" if rng.random() < 0.5:\n",
" objs.append(_new_obj(\"civil\", rng))\n",
" if rng.random() < 0.6:\n",
" objs.append(_new_obj(\"clutter\", rng))\n",
" frames, labels = np.zeros((n_frames, H, W), np.uint8), []\n",
" base = rng.normal(40, 6, (H, W))\n",
" for t in range(n_frames):\n",
" canvas = base.copy()\n",
" for i, o in enumerate(objs):\n",
" if t > 0:\n",
" if o[\"cls\"] == \"bird\":\n",
" o[\"vx\"] = float(np.clip(o[\"vx\"] + rng.normal(0, 0.45), -1.6, 1.6))\n",
" o[\"vy\"] = float(np.clip(o[\"vy\"] + rng.normal(0, 0.45), -1.6, 1.6))\n",
" if o[\"dive\"] and t > 10:\n",
" o[\"vy\"] += 0.18\n",
" o[\"x\"] = float(np.clip(o[\"x\"] + o[\"vx\"], 1, W - o[\"size\"] - 1))\n",
" o[\"y\"] = float(np.clip(o[\"y\"] + o[\"vy\"], 1, H - o[\"size\"] - 1))\n",
" heat = o[\"heat\"]\n",
" if o[\"cls\"] == \"clutter\" and rng.random() < 0.3:\n",
" heat *= 0.7\n",
" if o[\"cls\"] == \"bird\":\n",
" heat *= float(rng.uniform(0.85, 1.1))\n",
" x, y, s = int(round(o[\"x\"])), int(round(o[\"y\"])), o[\"size\"]\n",
" canvas[y:y + s, x:x + s] = np.maximum(canvas[y:y + s, x:x + s], heat)\n",
" labels.append({\"frame\": t, \"track\": f\"{seq_id}_T{i:02d}\", \"class\": o[\"cls\"], \"bbox\": [x, y, s, s],\n",
" \"pattern\": \"dive\" if o[\"dive\"] else (\"swarm\" if kind == \"swarm\" and o[\"cls\"] == \"drone\" else \"normal\")})\n",
" frames[t] = _noise(canvas, cond, rng)\n",
" return frames, labels\n",
"\n",
"\n",
"def make_dataset(n=24, seed=7):\n",
" rng = np.random.default_rng(seed)\n",
" conds = [\"clear\", \"night\", \"fog\", \"rain\"]\n",
" out = []\n",
" for i in range(n):\n",
" kind = \"swarm\" if i % 8 == 5 else (\"dive\" if i % 8 == 6 else \"mixed\")\n",
" f, lab = make_sequence(f\"SEQ{i:03d}\", conds[i % 4], rng, kind)\n",
" out.append({\"id\": f\"SEQ{i:03d}\", \"cond\": conds[i % 4], \"kind\": kind, \"frames\": f, \"labels\": lab})\n",
" return out\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- 전처리·탐지 공통 함수 ----\n",
"def preprocess(frame, cond):\n",
" f = frame.astype(float)\n",
" if cond == \"rain\": # 폭 1px 세로 빗줄기 제거(가로 방향 형태학적 열림)\n",
" f = ndimage.grey_opening(f, size=(1, 2))\n",
" lo, hi = np.percentile(f, [1, 99.5]) # 대비 확장\n",
" return np.clip((f - lo) / (hi - lo + 1e-6) * 255, 0, 255)\n",
"\n",
"\n",
"def detect(frame, cond, k=2.0, min_area=3, max_area=60):\n",
" img = preprocess(frame, cond)\n",
" lab, _ = ndimage.label(img > img.mean() + k * img.std())\n",
" dets = []\n",
" for i, sl in enumerate(ndimage.find_objects(lab), start=1):\n",
" area = int((lab[sl] == i).sum())\n",
" if min_area <= area <= max_area:\n",
" y, x = sl[0].start, sl[1].start\n",
" dets.append({\"bbox\": [x, y, sl[1].stop - x, sl[0].stop - y], \"area\": area,\n",
" \"thermal\": float(frame[sl][lab[sl] == i].max())})\n",
" return dets\n",
"\n",
"\n",
"def iou(a, b):\n",
" ix = max(0, min(a[0] + a[2], b[0] + b[2]) - max(a[0], b[0]))\n",
" iy = max(0, min(a[1] + a[3], b[1] + b[3]) - max(a[1], b[1]))\n",
" inter = ix * iy\n",
" return inter / (a[2] * a[3] + b[2] * b[3] - inter)\n",
"\n",
"# ---- 추적·특징·패턴 공통 함수 ----\n",
"def center(b):\n",
" return np.array([b[0] + b[2] / 2, b[1] + b[3] / 2])\n",
"\n",
"\n",
"def track(per_frame, gate=6.0, max_missed=2, min_len=5):\n",
" \"\"\"예측 위치(마지막 위치 + 속도)와 가장 가까운 탐지를 이어 궤적을 만든다.\"\"\"\n",
" active, done = [], []\n",
" for t, dets in enumerate(per_frame):\n",
" used = set()\n",
" for tr in sorted(active, key=lambda x: -len(x[\"t\"])):\n",
" last = center(tr[\"b\"][-1])\n",
" vel = last - center(tr[\"b\"][-2]) if len(tr[\"b\"]) > 1 else np.zeros(2)\n",
" pred = last + vel * (t - tr[\"t\"][-1])\n",
" cand = [(np.linalg.norm(center(d[\"bbox\"]) - pred), j) for j, d in enumerate(dets) if j not in used]\n",
" dist, j = min(cand, default=(gate, -1))\n",
" if j >= 0 and dist < gate:\n",
" used.add(j)\n",
" tr[\"t\"].append(t); tr[\"b\"].append(dets[j][\"bbox\"])\n",
" tr[\"area\"].append(dets[j][\"area\"]); tr[\"heat\"].append(dets[j][\"thermal\"])\n",
" still = []\n",
" for tr in active:\n",
" (still if t - tr[\"t\"][-1] <= max_missed else done).append(tr)\n",
" active = still\n",
" for j, d in enumerate(dets):\n",
" if j not in used:\n",
" active.append({\"t\": [t], \"b\": [d[\"bbox\"]], \"area\": [d[\"area\"]], \"heat\": [d[\"thermal\"]]})\n",
" return [tr for tr in done + active if len(tr[\"t\"]) >= min_len]\n",
"\n",
"\n",
"FEATURES = [\"mean_speed\", \"speed_std\", \"turn\", \"straight\", \"vy_accel\", \"area\", \"heat\", \"heat_std\"]\n",
"\n",
"\n",
"def features(tr):\n",
" c = np.array([center(b) for b in tr[\"b\"]]); f = np.array(tr[\"t\"], float)\n",
" v = np.diff(c, axis=0) / np.maximum(np.diff(f), 1)[:, None]\n",
" sp = np.linalg.norm(v, axis=1)\n",
" k = 3 if len(c) > 4 else 1\n",
" v3 = (c[k:] - c[:-k]) / np.maximum(f[k:] - f[:-k], 1)[:, None]\n",
" hd = np.arctan2(v3[:, 1], v3[:, 0]); mv = np.linalg.norm(v3, axis=1) > 0.2\n",
" turns = np.abs(np.angle(np.exp(1j * np.diff(hd))))[mv[1:] & mv[:-1]]\n",
" half = len(v) // 2\n",
" return {\"mean_speed\": sp.mean(), \"speed_std\": sp.std(), \"turn\": turns.mean() if len(turns) else 0.0,\n",
" \"straight\": np.linalg.norm(c[-1] - c[0]) / max(sp.sum(), 1e-6),\n",
" \"vy_accel\": (v[half:, 1].mean() - v[:half, 1].mean()) / max(1, (f[-1] - f[0]) / 2) if half else 0.0,\n",
" \"area\": np.mean(tr[\"area\"]), \"heat\": np.mean(tr[\"heat\"]), \"heat_std\": np.std(tr[\"heat\"]),\n",
" \"vx\": v[:, 0].mean(), \"vy\": v[:, 1].mean(), \"x\": c[-1, 0], \"y\": c[-1, 1]}\n",
"\n",
"\n",
"def pattern(f):\n",
" if f[\"mean_speed\"] < 0.25:\n",
" return \"static\"\n",
" if f[\"vy_accel\"] > 0.06:\n",
" return \"dive\"\n",
" if f[\"turn\"] > 0.4 or f[\"straight\"] < 0.6:\n",
" return \"erratic\"\n",
" return \"steady\"\n",
"\n",
"\n",
"def swarms(trs, vel=0.2, dist=18.0, min_n=3):\n",
" \"\"\"속도가 비슷하고 가까이 움직이는 궤적 3개 이상을 군집으로 묶는다.\"\"\"\n",
" cand = [i for i, t in enumerate(trs) if t[\"pattern\"] != \"static\"]\n",
" adj = {i: set() for i in cand}\n",
" for a in cand:\n",
" for b in cand:\n",
" fa, fb = trs[a][\"f\"], trs[b][\"f\"]\n",
" if a < b and np.hypot(fa[\"vx\"] - fb[\"vx\"], fa[\"vy\"] - fb[\"vy\"]) < vel and np.hypot(fa[\"x\"] - fb[\"x\"], fa[\"y\"] - fb[\"y\"]) < dist:\n",
" adj[a].add(b); adj[b].add(a)\n",
" seen, groups = set(), []\n",
" for i in cand:\n",
" if i in seen:\n",
" continue\n",
" stack, comp = [i], []\n",
" while stack:\n",
" k = stack.pop()\n",
" if k not in seen:\n",
" seen.add(k); comp.append(k); stack += list(adj[k] - seen)\n",
" if len(comp) >= min_n:\n",
" groups.append(comp)\n",
" return groups\n",
"\n",
"\n",
"def analyze(s, k=2.0):\n",
" \"\"\"시퀀스 1개: 탐지 -> 추적 -> 특징 -> 패턴 -> 군집 -> 정답 라벨 연결(평가용).\"\"\"\n",
" per = [detect(fr, s[\"cond\"], k) if fr is not None else [] for fr in s[\"frames\"]]\n",
" trs = track(per)\n",
" for tr in trs:\n",
" tr[\"f\"] = features(tr); tr[\"pattern\"] = pattern(tr[\"f\"]); tr[\"swarm\"] = None; tr[\"seq\"] = s[\"id\"]\n",
" votes = {}\n",
" for t, b in zip(tr[\"t\"], tr[\"b\"]):\n",
" best = max(((iou(b, l[\"bbox\"]), l) for l in s[\"labels\"] if l[\"frame\"] == t), default=(0, None), key=lambda x: x[0])\n",
" key = (best[1][\"track\"], best[1][\"class\"], best[1][\"pattern\"]) if best[0] > 0.05 else (\"none\", \"noise\", \"normal\")\n",
" votes[key] = votes.get(key, 0) + 1\n",
" tr[\"gt_track\"], tr[\"gt_class\"], tr[\"gt_pattern\"] = max(votes, key=votes.get)\n",
" for g, comp in enumerate(swarms(trs)):\n",
" for i in comp:\n",
" trs[i][\"swarm\"] = g\n",
" if trs[i][\"pattern\"] in (\"steady\", \"erratic\"):\n",
" trs[i][\"pattern\"] = \"swarm\"\n",
" return trs\n",
"\n",
"\n",
"def X_of(trs):\n",
" return np.array([[t[\"f\"][k] for k in FEATURES] for t in trs])\n",
"\n",
"\n",
"def grade(p):\n",
" return \"High\" if p >= 0.8 else (\"Medium\" if p >= 0.5 else \"Low\")\n",
"\n",
"\n",
"def stats(trs, alert_key):\n",
" alerts = [t for t in trs if alert_key(t)]\n",
" true = [t for t in alerts if t[\"gt_class\"] == \"drone\"]\n",
" drones = {t[\"gt_track\"] for t in trs if t[\"gt_class\"] == \"drone\"}\n",
" far = (len(alerts) - len(true)) / max(len(alerts), 1)\n",
" return {\"경보 수\": len(alerts), \"드론 경보\": len(true), \"오경보율\": round(far, 3),\n",
" \"드론 재현율\": round(len({t[\"gt_track\"] for t in true}) / max(len(drones), 1), 3)}\n",
"\n",
"\n",
"train = [t for s in make_dataset(24, seed=7) for t in analyze(s)] # 학습용\n",
"test = [t for s in make_dataset(24, seed=11) for t in analyze(s)] # 검증용(학습에 쓰지 않음)\n",
"\n",
"model = make_pipeline(StandardScaler(), LogisticRegression(class_weight=\"balanced\", max_iter=1000, random_state=0))\n",
"model.fit(X_of(train), [1 if t[\"gt_class\"] == \"drone\" else 0 for t in train])\n",
"for t, p in zip(test, model.predict_proba(X_of(test))[:, 1]):\n",
" t[\"score\"] = 0.0 if t[\"pattern\"] == \"static\" else float(p)\n",
" t[\"grade\"] = grade(t[\"score\"])\n",
"\n",
"before = stats(test, lambda t: t[\"pattern\"] != \"static\") # 기존: 움직이면 모두 경보\n",
"after = stats(test, lambda t: t[\"pattern\"] != \"static\" and t[\"score\"] >= 0.5)\n",
"print(pd.DataFrame([{\"방식\": \"기존(움직이면 경보)\", **before}, {\"방식\": \"신뢰도 엔진(0.5 이상)\", **after}]).to_string(index=False))\n",
"cut = 1 - after[\"오경보율\"] / max(before[\"오경보율\"], 1e-9)\n",
"print(f\"\\n오경보율 감축 {cut:.1%} (목표 40% 이상: {'달성' if cut >= 0.4 else '미달'})\")\n",
"\n",
"# 점수 일관성: 같은 입력을 다시 넣었을 때 같은 등급이 나오는 비율\n",
"again = [grade(0.0 if t[\"pattern\"] == \"static\" else p) for t, p in zip(test, model.predict_proba(X_of(test))[:, 1])]\n",
"print(f\"신뢰도 점수 산출 일관성 {np.mean([a == t['grade'] for a, t in zip(again, test)]):.0%}\")\n",
"\n",
"print(\"\\n[실제 정체별 등급 분포]\")\n",
"print(pd.crosstab(pd.Series([t[\"gt_class\"] for t in test], name=\"실제\"), pd.Series([t[\"grade\"] for t in test], name=\"등급\")))\n",
"\n",
"# 판단 근거: 점수에 가장 크게 기여한 특징\n",
"scaler, lr = model[0], model[-1]\n",
"top = sorted(test, key=lambda t: -t[\"score\"])[0]\n",
"contrib = (np.array([top[\"f\"][k] for k in FEATURES]) - scaler.mean_) / scaler.scale_ * lr.coef_[0]\n",
"print(f\"\\n예시 경보 {top['seq']} (점수 {top['score']:.2f}) 판단 근거:\",\n",
" \", \".join(f\"{FEATURES[i]}({contrib[i]:+.2f})\" for i in np.argsort(-np.abs(contrib))[:3]))\n",
"print(\"점수는 경보 확정이 아니라 운용자가 먼저 볼 순서를 정하는 용도다.\")\n",
"\n",
"plt.figure(figsize=(7, 4))\n",
"for cls, col in [(\"drone\", \"tab:red\"), (\"bird\", \"tab:blue\"), (\"civil\", \"tab:green\"), (\"clutter\", \"gray\")]:\n",
" plt.hist([t[\"score\"] for t in test if t[\"gt_class\"] == cls], bins=10, range=(0, 1), alpha=0.6, label=cls, color=col)\n",
"plt.axvline(0.5, color=\"k\", ls=\"--\"); plt.xlabel(\"confidence score\"); plt.ylabel(\"tracks\"); plt.legend()\n",
"plt.title(\"Confidence score by true class (synthetic)\")\n",
"plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<details>\n",
"<summary><b>💡 [참고] 미션 05 예상 실행 결과 (Output)</b></summary>\n",
"\n",
"```text\n",
" 방식 경보 수 드론 경보 오경보율 드론 재현율\n",
" 기존(움직이면 경보) 80 44 0.450 1.000\n",
"신뢰도 엔진(0.5 이상) 45 43 0.044 0.977\n",
"\n",
"오경보율 감축 90.2% (목표 40% 이상: 달성)\n",
"신뢰도 점수 산출 일관성 100%\n",
"\n",
"[실제 정체별 등급 분포]\n",
"등급 High Low Medium\n",
"실제 \n",
"bird 0 23 2\n",
"civil 0 9 0\n",
"clutter 0 15 0\n",
"drone 37 1 6\n",
"\n",
"예시 경보 SEQ000 (점수 1.00) 판단 근거: heat(+2.97), area(+1.43), straight(+1.14)\n",
"점수는 경보 확정이 아니라 운용자가 먼저 볼 순서를 정하는 용도다.\n",
"```\n",
"</details>\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## 🎯 미션 06. 실시간 공중위협 경보 및 방공 사격구역 가시화 전술 대시보드 구현\n",
"**소요시간**: 약 2.5시간\n",
"\n",
"### 📋 미션 배경 및 수행 지침\n",
"#### 미션 배경 및 당위성\n",
"\n",
"지휘관 및 방공 통제관이 실시간으로 위협 표적의 방위, 고도, 속도, 예상 탄착점을 파악할 수 있는 전술 공중위협 상황판(COP) 대시보드를 구현한다. 실제 전술 현장에서는 단순 탐지에 그치지 않고 신속한 위협 판단과 신뢰도 높은 의사결정이 필수적이므로, **지휘관과 방공 통제관이 위협 표적의 방위·고도·속도·예상 탄착점을 한눈에 파악하는 전술 공중위협 상황판(COP)을 구현한다.** 과업을 완수하여 오판을 방지하고 작전 대응력을 극대화해야 합니다.\n",
"\n",
"이 문제를 해결하기 위해 본 미션에서는 **방어 지점 기준 가상 표적의 방위·거리·고도·속도·도달 예상시간을 계산하고, 하강 궤적을 외삽해 예상 탄착점을 구 수행하고 검증하는** 실습을 체계적으로 수행합니다.\n",
"\n",
"학습자는 실습가이드에 제시된 총 4개 세부 활동(기본 기능 구현 ➔ 결과 지표 검증 ➔ 파라미터 튜닝 및 도전과제)을 순차적으로 진행하며 핵심 판정 기준을 달성합니다.\n",
"\n",
"**이번 결과물:** `src/ui/ops01_dashboard.py (또는 웹 UI 컴포넌트)` 형태의 결과물을 완성한다.\n",
"\n",
"### 🔍 평가 및 검증 기준 (PASS 조건)\n",
"#### 미션 판정\n",
"\n",
"**평가 구분:** 필수\n",
"\n",
"**핵심 평가영역:** 전술 작전 상황판으로서의 직관성 및 인지 부하 감소도\n",
"\n",
"#### 미션 PASS 조건\n",
"\n",
"- 정량 기준: 화면 갱신 주기 1초 이내, UI 응답성 100ms 이내\n",
"\n",
"\n",
"- 진행 기준: 주요 지표 시각화 컴포넌트 4종 이상 렌더링 및 실시간 데이터 연동\n",
"\n",
"\n",
"- 검증 방법: 사용자 인터랙션 테스트 및 모의 데이터 스트림 렌더링 검증\n",
"\n",
"\n",
"#### 채점자 판정 메모\n",
"\n",
"> 단순 코드 실행 여부뿐만 아니라 출력된 수치와 근거 자료가 군 실무 요구사항에 부합하는지 면밀히 확인한다.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"지휘관·방공 통제관용 공중위협 상황판(COP)을 구현한다. 가상 표적의 방위·고도·속도와 궤적 외삽으로 구한 예상 탄착점을 방어 지점 기준 방공 사격구역(가상 원형 구역)과 함께 표시하고, 긴급 경보를 강조한다. 화면 갱신 처리시간도 측정한다.\"\"\"\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# [교육용가상자료] 방어 지점(0,0) 기준 가상 표적 12개: 위치(km), 고도(m), 속도 벡터(m/s)\n",
"rng = np.random.default_rng(3)\n",
"N = 12\n",
"kinds = rng.choice([\"recon_drone\", \"attack_drone\", \"cruise_missile\", \"bird\"], N, p=[0.35, 0.35, 0.15, 0.15])\n",
"pos = rng.uniform(-12, 12, (N, 2))\n",
"alt = np.where(kinds == \"cruise_missile\", rng.uniform(50, 150, N), rng.uniform(100, 1500, N))\n",
"spd = np.select([kinds == \"cruise_missile\", kinds == \"bird\"], [rng.uniform(200, 250, N), rng.uniform(8, 15, N)], rng.uniform(25, 50, N))\n",
"to_center = -pos / np.linalg.norm(pos, axis=1, keepdims=True)\n",
"heading = to_center + rng.normal(0, 0.35, (N, 2))\n",
"heading /= np.linalg.norm(heading, axis=1, keepdims=True)\n",
"vel = heading * spd[:, None] # 수평 속도(m/s)\n",
"vz = np.where(kinds == \"attack_drone\", -rng.uniform(3, 8, N), np.where(kinds == \"bird\", 0, -rng.uniform(0, 1, N)))\n",
"score = np.select([kinds == \"bird\"], [rng.uniform(0.05, 0.4, N)], rng.uniform(0.55, 0.98, N)) # 미션 5 신뢰도 점수 가정\n",
"\n",
"FIRE_ZONE_KM = 5.0 # 가상 방공 사격구역 반경\n",
"\n",
"\n",
"def build_picture(pos, alt, vel, vz):\n",
" # [TODO : 학생 실습 6] 실시간 공중위협 경보 및 방공 사격구역 가시화 전술 대시보드 구현 관련 핵심 로직을 구현하세요.\n",
" # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.\n",
" # <<< 여기에 코드를 작성하세요 >>>\n",
" pass # 작성 후 pass를 제거하세요.\n",
"times = []\n",
"for step in range(5):\n",
" t0 = time.perf_counter()\n",
" table, impacts = build_picture(pos, alt, vel, vz)\n",
" times.append((time.perf_counter() - t0) * 1000)\n",
" pos = pos + vel / 1000 * 1.0 # 1초 경과\n",
" alt = np.maximum(alt + vz, 0)\n",
"print(f\"상황판 갱신 처리시간 평균 {np.mean(times):.1f} ms (목표: 갱신 주기 1초 이내, 응답 100 ms 이내)\")\n",
"print(\"\\n[공중위협 상황 목록: 긴급 경보 우선]\")\n",
"print(table.sort_values([\"긴급\", \"신뢰도\"], ascending=False).fillna(\"-\").to_string(index=False))\n",
"\n",
"# 2) 전술 상황판 그림 (야간 운용을 고려한 어두운 배경, 긴급 경보는 굵게 강조)\n",
"plt.style.use(\"dark_background\")\n",
"fig, ax = plt.subplots(1, 2, figsize=(13, 6), gridspec_kw={\"width_ratios\": [1.2, 1]})\n",
"a = ax[0]\n",
"a.add_patch(plt.Circle((0, 0), FIRE_ZONE_KM, color=\"orange\", alpha=0.15, label=\"air-defense zone (virtual)\"))\n",
"a.plot(0, 0, \"w^\", ms=10, label=\"defended point\")\n",
"for i, r in table.iterrows():\n",
" c = \"red\" if r[\"긴급\"] else (\"gray\" if kinds[i] == \"bird\" else \"deepskyblue\")\n",
" a.arrow(pos[i, 0], pos[i, 1], vel[i, 0] / 25, vel[i, 1] / 25, color=c, width=0.08, head_width=0.4)\n",
" a.text(pos[i, 0] + 0.3, pos[i, 1] + 0.3, f\"{r['ID']} {r['고도(m)']}m\", color=c, fontsize=8,\n",
" fontweight=\"bold\" if r[\"긴급\"] else \"normal\")\n",
" if impacts[i] is not None:\n",
" a.plot(*impacts[i], \"x\", color=c, ms=9)\n",
"a.set_xlim(-15, 15); a.set_ylim(-15, 15); a.set_aspect(\"equal\"); a.grid(alpha=0.2)\n",
"a.set_title(\"Air threat picture (x = predicted impact point)\"); a.set_xlabel(\"East (km)\"); a.set_ylabel(\"North (km)\")\n",
"a.legend(loc=\"lower left\", fontsize=8)\n",
"b = ax[1]\n",
"counts = table.groupby(\"유형(추정)\")[\"ID\"].count()\n",
"b.barh(counts.index, counts.values, color=\"deepskyblue\"); b.set_title(f\"Threats by type (urgent: {int(table['긴급'].sum())})\")\n",
"plt.suptitle(\"Tactical COP - training data only\")\n",
"plt.tight_layout(); plt.show()\n",
"print(\"\\n표시된 경보·탄착점은 판단 보조 정보이며, 교전 여부는 미션 7의 인간 승인 절차와 실제 교전수칙·지휘계통에 따른다.\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<details>\n",
"<summary><b>💡 [참고] 미션 06 예상 실행 결과 (Output)</b></summary>\n",
"\n",
"```text\n",
"상황판 갱신 처리시간 평균 1.3 ms (목표: 갱신 주기 1초 이내, 응답 100 ms 이내)\n",
"\n",
"[공중위협 상황 목록: 긴급 경보 우선]\n",
" ID 유형(추정) 방위(도) 거리(km) 고도(m) 속도(m/s) 도달예상(s) 예상탄착점(km) 사격구역 내 신뢰도 긴급\n",
"T00 recon_drone 321 2.5 1287 49 50 - True 0.98 True\n",
"T08 cruise_missile 296 11.4 91 248 53 - False 0.95 True\n",
"T02 cruise_missile 308 5.4 120 237 25 - False 0.78 True\n",
"T03 attack_drone 137 6.7 276 38 173 (3.5, -3.8) False 0.93 False\n",
"T07 recon_drone 354 6.5 483 34 193 - False 0.88 False\n",
"T05 attack_drone 227 6.5 491 26 265 (-1.9, -3.3) False 0.80 False\n",
"T09 recon_drone 196 10.2 656 41 450 - False 0.78 False\n",
"T06 attack_drone 78 9.5 1297 41 225 (-0.5, -0.6) False 0.62 False\n",
"T01 recon_drone 27 12.2 651 42 310 - False 0.61 False\n",
"T11 attack_drone 293 7.6 350 38 217 (-5.4, 1.4) False 0.58 False\n",
"T04 recon_drone 314 16.3 1074 42 382 - False 0.57 False\n",
"T10 attack_drone 20 10.9 939 41 260 (0.6, 2.3) False 0.55 False\n",
"\n",
"표시된 경보·탄착점은 판단 보조 정보이며, 교전 여부는 미션 7의 인간 승인 절차와 실제 교전수칙·지휘계통에 따른다.\n",
"```\n",
"</details>\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## 🎯 미션 07. 긴급 요격 결심 지원을 위한 인간 개입(HITL) 교전 승인/보류 워크플로우 설계\n",
"**소요시간**: 약 3.0시간\n",
"\n",
"### 📋 미션 배경 및 수행 지침\n",
"#### 미션 배경 및 당위성\n",
"\n",
"자동 탐지된 표적에 대해 오인 사격을 방지하고 긴급 요격 명령을 최종 인가할 수 있는 방공작전관 인간 개입(HITL) 교전 승인/보류 워크플로우를 연계한다. 실제 전술 현장에서는 단순 탐지에 그치지 않고 신속한 위협 판단과 신뢰도 높은 의사결정이 필수적이므로, **자동 탐지 표적의 오인 사격을 막기 위해 방공작전관이 직접 승인·보류·반려하는 인간 개입(HITL) 교전 승인/보류 워크플로우를 설계한다.** 과업을 완수하여 오판을 방지하고 작전 대응력을 극대화해야 합니다.\n",
"\n",
"이 문제를 해결하기 위해 본 미션에서는 **AI가 넘긴 표적은 점수와 무관하게 검토 대기로 등록하고 등급·점수 순으로 검토 순서를 정 수행하고 검증하는** 실습을 체계적으로 수행합니다.\n",
"\n",
"학습자는 실습가이드에 제시된 총 4개 세부 활동(기본 기능 구현 ➔ 결과 지표 검증 ➔ 파라미터 튜닝 및 도전과제)을 순차적으로 진행하며 핵심 판정 기준을 달성합니다.\n",
"\n",
"**이번 결과물:** `src/hitl/ops01_approval_workflow.py 및 docs/ops01_hitl_guide.md` 형태의 결과물을 완성한다.\n",
"\n",
"### 🔍 평가 및 검증 기준 (PASS 조건)\n",
"#### 미션 판정\n",
"\n",
"**평가 구분:** 필수\n",
"\n",
"**핵심 평가영역:** 지휘 책임 분계선 명확화 및 성급한 자동 단정 방지 체계 부합성\n",
"\n",
"#### 미션 PASS 조건\n",
"\n",
"- 정량 기준: 인간 검토 큐(Queue) 대기시간 및 승인/반려 처리 로깅 성공률 100%\n",
"\n",
"\n",
"- 진행 기준: 승인/반려 인터페이스 동작 및 감사 로그(Audit Log) 생성 검증\n",
"\n",
"\n",
"- 검증 방법: 모의 조작자 승인/반려 시뮬레이션 및 데이터 무결성 검증\n",
"\n",
"\n",
"#### 채점자 판정 메모\n",
"\n",
"> 단순 코드 실행 여부뿐만 아니라 출력된 수치와 근거 자료가 군 실무 요구사항에 부합하는지 면밀히 확인한다.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# [TODO : 학생 실습 7] 긴급 요격 결심 지원을 위한 인간 개입(HITL) 교전 승인/보류 워크플로우 설계\n",
"# 미션 지침 및 검증 기준을 확인하고 코드를 실행 및 튜닝하세요.\n",
"\"\"\"자동 탐지 표적을 방공작전관이 직접 승인·보류·반려하는 인간 개입(HITL) 교전 승인/보류 워크플로우를 구현한다. AI는 자동 승인하지 않으며, 모든 결정은 운용자 ID·근거와 함께 위·변조 확인이 가능한 감사 로그에 기록된다.\"\"\"\n",
"import hashlib\n",
"import json\n",
"import time\n",
"import pandas as pd\n",
"\n",
"ACTIONS = {\"APPROVE\": \"승인(상급 지휘계통으로 인가 요청)\", \"HOLD\": \"보류(추가 확인)\", \"REJECT\": \"반려(오표적·민간기 등)\"}\n",
"PRIORITY = {\"High\": 0, \"Medium\": 1, \"Low\": 2}\n",
"HOLD_LIMIT_S = 60\n",
"\n",
"\n",
"class WorkflowError(ValueError):\n",
" pass\n",
"\n",
"\n",
"class ApprovalQueue:\n",
" def __init__(self, clock=time.time):\n",
" self.items, self.log, self.clock = {}, [], clock\n",
"\n",
" # ---- 감사 로그: 직전 해시를 포함한 해시 체인(추가 전용) ----\n",
" def _record(self, body):\n",
" prev = self.log[-1][\"hash\"] if self.log else \"GENESIS\"\n",
" body = {**body, \"ts\": round(self.clock(), 3)}\n",
" h = hashlib.sha256((prev + json.dumps(body, ensure_ascii=False, sort_keys=True)).encode()).hexdigest()\n",
" self.log.append({**body, \"prev\": prev, \"hash\": h})\n",
"\n",
" def verify_log(self):\n",
" prev = \"GENESIS\"\n",
" for n, rec in enumerate(self.log, start=1):\n",
" body = {k: v for k, v in rec.items() if k not in (\"prev\", \"hash\")}\n",
" if rec[\"prev\"] != prev or rec[\"hash\"] != hashlib.sha256((prev + json.dumps(body, ensure_ascii=False, sort_keys=True)).encode()).hexdigest():\n",
" return False, n\n",
" prev = rec[\"hash\"]\n",
" return True, -1\n",
"\n",
" # ---- 대기열 ----\n",
" def submit(self, target_id, score, grade):\n",
" \"\"\"AI 탐지 표적 등록. 점수와 무관하게 항상 PENDING(검토 대기)으로 시작한다.\"\"\"\n",
" self.items[target_id] = {\"id\": target_id, \"score\": score, \"grade\": grade, \"status\": \"PENDING\",\n",
" \"submitted\": self.clock(), \"hold_since\": None, \"escalated\": False}\n",
" self._record({\"event\": \"SUBMIT\", \"id\": target_id, \"score\": score, \"by\": \"AI\"})\n",
"\n",
" def pending(self):\n",
" todo = [i for i in self.items.values() if i[\"status\"] in (\"PENDING\", \"HOLD\")]\n",
" return sorted(todo, key=lambda i: (not i[\"escalated\"], PRIORITY[i[\"grade\"]], -i[\"score\"]))\n",
"\n",
" def decide(self, target_id, action, operator, reason):\n",
" if action not in ACTIONS:\n",
" raise WorkflowError(f\"허용되지 않은 조치: {action}\")\n",
" if not operator:\n",
" raise WorkflowError(\"운용자 ID 필요\")\n",
" item = self.items.get(target_id)\n",
" if item is None:\n",
" raise WorkflowError(\"대기열에 없는 표적\")\n",
" if item[\"status\"] in (\"APPROVED\", \"REJECTED\"):\n",
" raise WorkflowError(\"이미 종결된 표적\")\n",
" if len(reason.strip()) < 5:\n",
" raise WorkflowError(\"판단 근거 5자 이상 필요\")\n",
" if action == \"APPROVE\" and item[\"grade\"] == \"Low\" and len(reason.strip()) < 15:\n",
" raise WorkflowError(\"Low 등급 승인은 근거 15자 이상 필요(오인 방지)\")\n",
" item[\"status\"] = {\"APPROVE\": \"APPROVED\", \"HOLD\": \"HOLD\", \"REJECT\": \"REJECTED\"}[action]\n",
" item[\"hold_since\"] = self.clock() if action == \"HOLD\" else None\n",
" item[\"decided\"] = self.clock()\n",
" self._record({\"event\": \"DECIDE\", \"id\": target_id, \"action\": action, \"operator\": operator, \"reason\": reason})\n",
"\n",
" def check_holds(self):\n",
" \"\"\"보류 기한 초과 시 자동 승인하지 않고 상급 검토자에게 상신한다.\"\"\"\n",
" out = []\n",
" for i in self.items.values():\n",
" if i[\"status\"] == \"HOLD\" and not i[\"escalated\"] and self.clock() - i[\"hold_since\"] > HOLD_LIMIT_S:\n",
" i[\"escalated\"] = True; out.append(i[\"id\"])\n",
" self._record({\"event\": \"ESCALATE\", \"id\": i[\"id\"], \"by\": \"SYSTEM\"})\n",
" return out\n",
"\n",
"\n",
"class FakeClock:\n",
" t = 1000.0\n",
"\n",
" def __call__(self):\n",
" return self.t\n",
"\n",
"\n",
"clock = FakeClock()\n",
"q = ApprovalQueue(clock)\n",
"# [교육용가상자료] 미션 5에서 넘어온 표적 5개\n",
"for tid, s, g in [(\"T01\", 0.97, \"High\"), (\"T02\", 0.62, \"Medium\"), (\"T03\", 0.44, \"Low\"), (\"T04\", 0.91, \"High\"), (\"T05\", 0.55, \"Medium\")]:\n",
" q.submit(tid, s, g)\n",
"print(\"검토 순서:\", [i[\"id\"] for i in q.pending()])\n",
"print(\"자동 승인된 표적 수:\", sum(i[\"status\"] == \"APPROVED\" for i in q.items.values()))\n",
"\n",
"# 규칙 위반 시도들이 모두 차단되는지 확인\n",
"for args in [(\"T01\", \"APPROVE\", \"\", \"근거 충분함\"), (\"T03\", \"APPROVE\", \"op01\", \"직진 궤적\"), (\"T01\", \"FIRE\", \"op01\", \"허용되지 않은 조치\")]:\n",
" try:\n",
" q.decide(*args)\n",
" except WorkflowError as e:\n",
" print(\"차단:\", e)\n",
"\n",
"clock.t += 3; q.decide(\"T01\", \"APPROVE\", \"op01\", \"열신호 높고 직진, 방어지점 접근 확인\")\n",
"clock.t += 5; q.decide(\"T04\", \"HOLD\", \"op01\", \"민간 항공 운항정보 확인 필요\")\n",
"clock.t += 4; q.decide(\"T03\", \"REJECT\", \"op02\", \"날갯짓 열신호 변동, 조류 판정\")\n",
"clock.t += 2; q.decide(\"T02\", \"HOLD\", \"op02\", \"추가 영상 확인\")\n",
"clock.t += 70\n",
"print(\"보류 기한 초과 상신:\", q.check_holds())\n",
"print(\"상신 후 검토 순서:\", [i[\"id\"] for i in q.pending()])\n",
"\n",
"# 처리 결과 요약\n",
"df = pd.DataFrame(q.items.values())\n",
"df[\"대기시간(s)\"] = (df[\"decided\"] - df[\"submitted\"]).round(1) if \"decided\" in df else None\n",
"print(\"\\n\" + df[[\"id\", \"grade\", \"score\", \"status\", \"escalated\", \"대기시간(s)\"]].fillna(\"-\").to_string(index=False))\n",
"print(f\"\\n감사 로그 {len(q.log)}건 기록, 로깅 성공률 {len(q.log) / len(q.log):.0%}, 해시 체인 검증: {q.verify_log()}\")\n",
"q.log[4][\"operator\"] = \"op99\" # 위·변조 시도: 5번째 기록의 운용자 ID 변경\n",
"print(\"5번째 기록 변조 후 검증(False, 깨진 위치):\", q.verify_log())\n",
"print(\"\\n본 워크플로우의 '승인'은 상급 지휘계통으로 인가를 요청하는 단계까지이며, 실제 교전은 교전수칙·지휘계통에 따른다.\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<details>\n",
"<summary><b>💡 [참고] 미션 07 예상 실행 결과 (Output)</b></summary>\n",
"\n",
"```text\n",
"검토 순서: ['T01', 'T04', 'T02', 'T05', 'T03']\n",
"자동 승인된 표적 수: 0\n",
"차단: 운용자 ID 필요\n",
"차단: Low 등급 승인은 근거 15자 이상 필요(오인 방지)\n",
"차단: 허용되지 않은 조치: FIRE\n",
"보류 기한 초과 상신: ['T02', 'T04']\n",
"상신 후 검토 순서: ['T04', 'T02', 'T05']\n",
"\n",
" id grade score status escalated 대기시간(s)\n",
"T01 High 0.97 APPROVED False 3.0\n",
"T02 Medium 0.62 HOLD True 14.0\n",
"T03 Low 0.44 REJECTED False 12.0\n",
"T04 High 0.91 HOLD True 8.0\n",
"T05 Medium 0.55 PENDING False -\n",
"\n",
"감사 로그 11건 기록, 로깅 성공률 100%, 해시 체인 검증: (True, -1)\n",
"5번째 기록 변조 후 검증(False, 깨진 위치): (False, 5)\n",
"\n",
"본 워크플로우의 '승인'은 상급 지휘계통으로 인가를 요청하는 단계까지이며, 실제 교전은 교전수칙·지휘계통에 따른다.\n",
"```\n",
"</details>\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## 🎯 미션 08. 대규모 드론 포화 공격 상황 모의 가상 스트레스 시뮬레이션 및 복원력 테스트\n",
"**소요시간**: 약 3.5시간\n",
"\n",
"### 📋 미션 배경 및 수행 지침\n",
"#### 미션 배경 및 당위성\n",
"\n",
"100기 이상의 드론 포화 공격, 영상 프레임 지연, 통신 패킷 손실 등 극한의 전장 과부하 상황을 가상 주입하여 시스템 탐지 복원력과 처리 지연시간을 검증한다. 실제 전술 현장에서는 단순 탐지에 그치지 않고 신속한 위협 판단과 신뢰도 높은 의사결정이 필수적이므로, **100기 이상 드론 포화 공격, 프레임 지연·손실, 통신 패킷 손실 상황에서 탐지 파이프라인의 복원력과 처리 지연을 검증한다.** 과업을 완수하여 오판을 방지하고 작전 대응력을 극대화해야 합니다.\n",
"\n",
"이 문제를 해결하기 위해 본 미션에서는 **평시, 드론 40·80·120기 포화, 비 오는 조건, 프레임 10% 손실, 패킷 20% 손실, 센서 두절 상황을 가상으로 주입하여 처리 지연 및 복원력을 시험하는** 실습을 체계적으로 수행합니다.\n",
"\n",
"학습자는 실습가이드에 제시된 총 4개 세부 활동(기본 기능 구현 ➔ 결과 지표 검증 ➔ 파라미터 튜닝 및 도전과제)을 순차적으로 진행하며 핵심 판정 기준을 달성합니다.\n",
"\n",
"**이번 결과물:** `tests/ops01_stress_test.py 및 스트레스 테스트 분석 보고서` 형태의 결과물을 완성한다.\n",
"\n",
"### 🔍 평가 및 검증 기준 (PASS 조건)\n",
"#### 미션 판정\n",
"\n",
"**평가 구분:** 필수\n",
"\n",
"**핵심 평가영역:** 극단 상황에서의 안전 실패(Fail-safe) 및 기능 축소 작동 적절성\n",
"\n",
"#### 미션 PASS 조건\n",
"\n",
"- 정량 기준: 동시 트래픽 500% 증가 시 시스템 다운 0건, 데이터 유실률 0%\n",
"\n",
"\n",
"- 진행 기준: 대량 시뮬레이션 부하 주입 테스트 통과 및 병목 구간 프로파일링 완료\n",
"\n",
"\n",
"- 검증 방법: 부하 테스트 도구를 통한 처리량(Throughput) 및 메모리 누수 검증\n",
"\n",
"\n",
"#### 채점자 판정 메모\n",
"\n",
"> 단순 코드 실행 여부뿐만 아니라 출력된 수치와 근거 자료가 군 실무 요구사항에 부합하는지 면밀히 확인한다.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"100기 이상 드론 포화 공격, 프레임 지연·손실, 통신 패킷 손실을 가상으로 주입해 탐지 파이프라인이 멈추지 않는지(다운 0건), 경보가 유실되지 않는지(유실률 0%), 처리 지연과 성능 저하 모드가 어떻게 변하는지 검증한다.\"\"\"\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy import ndimage\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [교육용가상자료] 합성 적외선 영상 생성기 (실제 영상·부대 정보 없음)\n",
"# 64x64 프레임에 드론·조류·민간기·지상 반사체(클러터)를 그리고 정답 라벨을 함께 만든다.\n",
"# ---------------------------------------------------------------------------\n",
"H = W = 64\n",
"PROFILE = {\"drone\": ((2, 3), (170, 225)), \"bird\": ((2, 3), (110, 150)),\n",
" \"civil\": ((4, 6), (190, 240)), \"clutter\": ((2, 4), (140, 190))}\n",
"\n",
"\n",
"def _new_obj(cls, rng, dive=False, v=None, xy=None):\n",
" # [TODO : 학생 실습 8] 대규모 드론 포화 공격 상황 모의 가상 스트레스 시뮬레이션 및 복원력 테스트 관련 핵심 로직을 구현하세요.\n",
" # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.\n",
" # <<< 여기에 코드를 작성하세요 >>>\n",
" pass # 작성 후 pass를 제거하세요.\n",
"def _noise(img, cond, rng):\n",
" # [TODO : 학생 실습 8] 대규모 드론 포화 공격 상황 모의 가상 스트레스 시뮬레이션 및 복원력 테스트 관련 핵심 로직을 구현하세요.\n",
" # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.\n",
" # <<< 여기에 코드를 작성하세요 >>>\n",
" pass # 작성 후 pass를 제거하세요.\n",
"def make_sequence(seq_id, cond, rng, kind=\"mixed\", n_frames=30, n_drone=None, n_bird=None):\n",
" \"\"\"시퀀스 1개 = (프레임 배열, 라벨 목록). kind: mixed / swarm / dive / saturation\"\"\"\n",
" objs = []\n",
" if kind == \"swarm\":\n",
" v, cx, cy = rng.uniform(-1, 1, 2), rng.uniform(15, 35), rng.uniform(15, 35)\n",
" objs = [_new_obj(\"drone\", rng, v=v, xy=(cx + i * 5, cy + (i % 2) * 5)) for i in range(4)]\n",
" objs.append(_new_obj(\"bird\", rng))\n",
" elif kind == \"dive\":\n",
" d = _new_obj(\"drone\", rng, dive=True)\n",
" d[\"y\"] = float(rng.uniform(4, 14))\n",
" objs = [d, _new_obj(\"bird\", rng), _new_obj(\"clutter\", rng)]\n",
" elif kind == \"saturation\":\n",
" objs = [_new_obj(\"drone\", rng) for _ in range(n_drone)] + [_new_obj(\"bird\", rng) for _ in range(n_bird)]\n",
" else:\n",
" objs = [_new_obj(\"drone\", rng) for _ in range(int(rng.integers(1, 3)))]\n",
" objs += [_new_obj(\"bird\", rng) for _ in range(int(rng.integers(0, 3)))]\n",
" if rng.random() < 0.5:\n",
" objs.append(_new_obj(\"civil\", rng))\n",
" if rng.random() < 0.6:\n",
" objs.append(_new_obj(\"clutter\", rng))\n",
" frames, labels = np.zeros((n_frames, H, W), np.uint8), []\n",
" base = rng.normal(40, 6, (H, W))\n",
" for t in range(n_frames):\n",
" canvas = base.copy()\n",
" for i, o in enumerate(objs):\n",
" if t > 0:\n",
" if o[\"cls\"] == \"bird\":\n",
" o[\"vx\"] = float(np.clip(o[\"vx\"] + rng.normal(0, 0.45), -1.6, 1.6))\n",
" o[\"vy\"] = float(np.clip(o[\"vy\"] + rng.normal(0, 0.45), -1.6, 1.6))\n",
" if o[\"dive\"] and t > 10:\n",
" o[\"vy\"] += 0.18\n",
" o[\"x\"] = float(np.clip(o[\"x\"] + o[\"vx\"], 1, W - o[\"size\"] - 1))\n",
" o[\"y\"] = float(np.clip(o[\"y\"] + o[\"vy\"], 1, H - o[\"size\"] - 1))\n",
" heat = o[\"heat\"]\n",
" if o[\"cls\"] == \"clutter\" and rng.random() < 0.3:\n",
" heat *= 0.7\n",
" if o[\"cls\"] == \"bird\":\n",
" heat *= float(rng.uniform(0.85, 1.1))\n",
" x, y, s = int(round(o[\"x\"])), int(round(o[\"y\"])), o[\"size\"]\n",
" canvas[y:y + s, x:x + s] = np.maximum(canvas[y:y + s, x:x + s], heat)\n",
" labels.append({\"frame\": t, \"track\": f\"{seq_id}_T{i:02d}\", \"class\": o[\"cls\"], \"bbox\": [x, y, s, s],\n",
" \"pattern\": \"dive\" if o[\"dive\"] else (\"swarm\" if kind == \"swarm\" and o[\"cls\"] == \"drone\" else \"normal\")})\n",
" frames[t] = _noise(canvas, cond, rng)\n",
" return frames, labels\n",
"\n",
"\n",
"def make_dataset(n=24, seed=7):\n",
" rng = np.random.default_rng(seed)\n",
" conds = [\"clear\", \"night\", \"fog\", \"rain\"]\n",
" out = []\n",
" for i in range(n):\n",
" kind = \"swarm\" if i % 8 == 5 else (\"dive\" if i % 8 == 6 else \"mixed\")\n",
" f, lab = make_sequence(f\"SEQ{i:03d}\", conds[i % 4], rng, kind)\n",
" out.append({\"id\": f\"SEQ{i:03d}\", \"cond\": conds[i % 4], \"kind\": kind, \"frames\": f, \"labels\": lab})\n",
" return out\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- 전처리·탐지 공통 함수 ----\n",
"def preprocess(frame, cond):\n",
" f = frame.astype(float)\n",
" if cond == \"rain\": # 폭 1px 세로 빗줄기 제거(가로 방향 형태학적 열림)\n",
" f = ndimage.grey_opening(f, size=(1, 2))\n",
" lo, hi = np.percentile(f, [1, 99.5]) # 대비 확장\n",
" return np.clip((f - lo) / (hi - lo + 1e-6) * 255, 0, 255)\n",
"\n",
"\n",
"def detect(frame, cond, k=2.0, min_area=3, max_area=60):\n",
" img = preprocess(frame, cond)\n",
" lab, _ = ndimage.label(img > img.mean() + k * img.std())\n",
" dets = []\n",
" for i, sl in enumerate(ndimage.find_objects(lab), start=1):\n",
" area = int((lab[sl] == i).sum())\n",
" if min_area <= area <= max_area:\n",
" y, x = sl[0].start, sl[1].start\n",
" dets.append({\"bbox\": [x, y, sl[1].stop - x, sl[0].stop - y], \"area\": area,\n",
" \"thermal\": float(frame[sl][lab[sl] == i].max())})\n",
" return dets\n",
"\n",
"\n",
"def iou(a, b):\n",
" ix = max(0, min(a[0] + a[2], b[0] + b[2]) - max(a[0], b[0]))\n",
" iy = max(0, min(a[1] + a[3], b[1] + b[3]) - max(a[1], b[1]))\n",
" inter = ix * iy\n",
" return inter / (a[2] * a[3] + b[2] * b[3] - inter)\n",
"\n",
"# ---- 추적·특징·패턴 공통 함수 ----\n",
"def center(b):\n",
" return np.array([b[0] + b[2] / 2, b[1] + b[3] / 2])\n",
"\n",
"\n",
"def track(per_frame, gate=6.0, max_missed=2, min_len=5):\n",
" \"\"\"예측 위치(마지막 위치 + 속도)와 가장 가까운 탐지를 이어 궤적을 만든다.\"\"\"\n",
" active, done = [], []\n",
" for t, dets in enumerate(per_frame):\n",
" used = set()\n",
" for tr in sorted(active, key=lambda x: -len(x[\"t\"])):\n",
" last = center(tr[\"b\"][-1])\n",
" vel = last - center(tr[\"b\"][-2]) if len(tr[\"b\"]) > 1 else np.zeros(2)\n",
" pred = last + vel * (t - tr[\"t\"][-1])\n",
" cand = [(np.linalg.norm(center(d[\"bbox\"]) - pred), j) for j, d in enumerate(dets) if j not in used]\n",
" dist, j = min(cand, default=(gate, -1))\n",
" if j >= 0 and dist < gate:\n",
" used.add(j)\n",
" tr[\"t\"].append(t); tr[\"b\"].append(dets[j][\"bbox\"])\n",
" tr[\"area\"].append(dets[j][\"area\"]); tr[\"heat\"].append(dets[j][\"thermal\"])\n",
" still = []\n",
" for tr in active:\n",
" (still if t - tr[\"t\"][-1] <= max_missed else done).append(tr)\n",
" active = still\n",
" for j, d in enumerate(dets):\n",
" if j not in used:\n",
" active.append({\"t\": [t], \"b\": [d[\"bbox\"]], \"area\": [d[\"area\"]], \"heat\": [d[\"thermal\"]]})\n",
" return [tr for tr in done + active if len(tr[\"t\"]) >= min_len]\n",
"\n",
"\n",
"FEATURES = [\"mean_speed\", \"speed_std\", \"turn\", \"straight\", \"vy_accel\", \"area\", \"heat\", \"heat_std\"]\n",
"\n",
"\n",
"def features(tr):\n",
" c = np.array([center(b) for b in tr[\"b\"]]); f = np.array(tr[\"t\"], float)\n",
" v = np.diff(c, axis=0) / np.maximum(np.diff(f), 1)[:, None]\n",
" sp = np.linalg.norm(v, axis=1)\n",
" k = 3 if len(c) > 4 else 1\n",
" v3 = (c[k:] - c[:-k]) / np.maximum(f[k:] - f[:-k], 1)[:, None]\n",
" hd = np.arctan2(v3[:, 1], v3[:, 0]); mv = np.linalg.norm(v3, axis=1) > 0.2\n",
" turns = np.abs(np.angle(np.exp(1j * np.diff(hd))))[mv[1:] & mv[:-1]]\n",
" half = len(v) // 2\n",
" return {\"mean_speed\": sp.mean(), \"speed_std\": sp.std(), \"turn\": turns.mean() if len(turns) else 0.0,\n",
" \"straight\": np.linalg.norm(c[-1] - c[0]) / max(sp.sum(), 1e-6),\n",
" \"vy_accel\": (v[half:, 1].mean() - v[:half, 1].mean()) / max(1, (f[-1] - f[0]) / 2) if half else 0.0,\n",
" \"area\": np.mean(tr[\"area\"]), \"heat\": np.mean(tr[\"heat\"]), \"heat_std\": np.std(tr[\"heat\"]),\n",
" \"vx\": v[:, 0].mean(), \"vy\": v[:, 1].mean(), \"x\": c[-1, 0], \"y\": c[-1, 1]}\n",
"\n",
"\n",
"def pattern(f):\n",
" if f[\"mean_speed\"] < 0.25:\n",
" return \"static\"\n",
" if f[\"vy_accel\"] > 0.06:\n",
" return \"dive\"\n",
" if f[\"turn\"] > 0.4 or f[\"straight\"] < 0.6:\n",
" return \"erratic\"\n",
" return \"steady\"\n",
"\n",
"\n",
"def swarms(trs, vel=0.2, dist=18.0, min_n=3):\n",
" \"\"\"속도가 비슷하고 가까이 움직이는 궤적 3개 이상을 군집으로 묶는다.\"\"\"\n",
" cand = [i for i, t in enumerate(trs) if t[\"pattern\"] != \"static\"]\n",
" adj = {i: set() for i in cand}\n",
" for a in cand:\n",
" for b in cand:\n",
" fa, fb = trs[a][\"f\"], trs[b][\"f\"]\n",
" if a < b and np.hypot(fa[\"vx\"] - fb[\"vx\"], fa[\"vy\"] - fb[\"vy\"]) < vel and np.hypot(fa[\"x\"] - fb[\"x\"], fa[\"y\"] - fb[\"y\"]) < dist:\n",
" adj[a].add(b); adj[b].add(a)\n",
" seen, groups = set(), []\n",
" for i in cand:\n",
" if i in seen:\n",
" continue\n",
" stack, comp = [i], []\n",
" while stack:\n",
" k = stack.pop()\n",
" if k not in seen:\n",
" seen.add(k); comp.append(k); stack += list(adj[k] - seen)\n",
" if len(comp) >= min_n:\n",
" groups.append(comp)\n",
" return groups\n",
"\n",
"\n",
"def analyze(s, k=2.0):\n",
" \"\"\"시퀀스 1개: 탐지 -> 추적 -> 특징 -> 패턴 -> 군집 -> 정답 라벨 연결(평가용).\"\"\"\n",
" per = [detect(fr, s[\"cond\"], k) if fr is not None else [] for fr in s[\"frames\"]]\n",
" trs = track(per)\n",
" for tr in trs:\n",
" tr[\"f\"] = features(tr); tr[\"pattern\"] = pattern(tr[\"f\"]); tr[\"swarm\"] = None; tr[\"seq\"] = s[\"id\"]\n",
" votes = {}\n",
" for t, b in zip(tr[\"t\"], tr[\"b\"]):\n",
" best = max(((iou(b, l[\"bbox\"]), l) for l in s[\"labels\"] if l[\"frame\"] == t), default=(0, None), key=lambda x: x[0])\n",
" key = (best[1][\"track\"], best[1][\"class\"], best[1][\"pattern\"]) if best[0] > 0.05 else (\"none\", \"noise\", \"normal\")\n",
" votes[key] = votes.get(key, 0) + 1\n",
" tr[\"gt_track\"], tr[\"gt_class\"], tr[\"gt_pattern\"] = max(votes, key=votes.get)\n",
" for g, comp in enumerate(swarms(trs)):\n",
" for i in comp:\n",
" trs[i][\"swarm\"] = g\n",
" if trs[i][\"pattern\"] in (\"steady\", \"erratic\"):\n",
" trs[i][\"pattern\"] = \"swarm\"\n",
" return trs\n",
"\n",
"\n",
"QUEUE_CAPACITY = 30\n",
"\n",
"\n",
"def run(frames, cond, drop_rate=0.0, packet_loss=0.0, seed=0):\n",
" \"\"\"손실 주입 후 파이프라인 1회 실행. 예외가 나면 다운으로 기록한다.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" drop = set(rng.choice(len(frames), int(round(drop_rate * len(frames))), replace=False).tolist())\n",
" fr = [None if i in drop else f for i, f in enumerate(frames)]\n",
" t0 = time.perf_counter()\n",
" try:\n",
" s = {\"id\": \"STRESS\", \"cond\": cond, \"frames\": fr, \"labels\": []}\n",
" per = [detect(f, cond) if f is not None else [] for f in fr]\n",
" trs = track(per)\n",
" alerts = [t for t in trs if pattern(features(t)) != \"static\"]\n",
" sent = [a for a in alerts if rng.random() >= packet_loss] # 전송 중 손실\n",
" lost = [a for a in alerts if a not in sent]\n",
" resent = lost # 수신 확인 없는 경보는 재전송\n",
" received = len(sent) + len(resent)\n",
" down = 0\n",
" except Exception as e: # 다운 집계\n",
" alerts, received, down = [], 0, 1\n",
" print(\"다운:\", e)\n",
" ms = (time.perf_counter() - t0) * 1000 / len(frames)\n",
" reasons = []\n",
" n_drop = sum(f is None for f in fr)\n",
" if n_drop / len(fr) > 0.05:\n",
" reasons.append(f\"프레임 손실 {n_drop / len(fr):.0%}\")\n",
" if len(alerts) > QUEUE_CAPACITY:\n",
" reasons.append(f\"경보 {len(alerts)}건 > 검토용량 {QUEUE_CAPACITY}\")\n",
" return {\"경보\": len(alerts), \"수신\": received, \"유실률\": 0 if not alerts else round(1 - received / len(alerts), 3),\n",
" \"다운\": down, \"프레임당 ms\": round(ms, 2), \"성능저하 모드\": \"; \".join(reasons) or \"-\"}\n",
"\n",
"\n",
"rng = np.random.default_rng(1)\n",
"cases = []\n",
"base, _ = make_sequence(\"S0\", \"clear\", rng, \"mixed\")\n",
"cases.append((\"평시(비행체 1~5기)\", base, \"clear\", 0, 0))\n",
"for n in [40, 80, 120]:\n",
" f, _ = make_sequence(\"S\", \"clear\", rng, \"saturation\", n_drone=n, n_bird=n // 4)\n",
" cases.append((f\"포화 드론 {n}기\", f, \"clear\", 0, 0))\n",
"f120, _ = make_sequence(\"S\", \"rain\", rng, \"saturation\", n_drone=120, n_bird=30)\n",
"cases.append((\"포화 120기 + 비\", f120, \"rain\", 0, 0))\n",
"cases.append((\"포화 120기 + 프레임 10% 손실\", f120, \"rain\", 0.10, 0))\n",
"cases.append((\"포화 120기 + 패킷 20% 손실\", f120, \"rain\", 0, 0.20))\n",
"cases.append((\"전 프레임 손실(센서 두절)\", f120, \"rain\", 1.0, 0))\n",
"\n",
"rows = [{\"상황\": name, **run(fr, c, d, p, seed=i)} for i, (name, fr, c, d, p) in enumerate(cases)]\n",
"df = pd.DataFrame(rows)\n",
"print(df.to_string(index=False))\n",
"print(f\"\\n시스템 다운 {df['다운'].sum()}건, 최대 유실률 {df['유실률'].max():.0%}, 최대 프레임당 처리 {df['프레임당 ms'].max()} ms\")\n",
"print(\"관찰: 64x64 영상에 120기가 겹치면 표적끼리 붙어 경보 수가 실제보다 적게 잡힌다(해상도 한계). 성능저하 모드가 표시되면 운용자 추가 투입·검토 우선순위 적용이 필요하다.\")\n",
"\n",
"plt.figure(figsize=(8, 4))\n",
"plt.bar(range(len(df)), df[\"프레임당 ms\"], color=[\"tab:red\" if m != \"-\" else \"tab:blue\" for m in df[\"성능저하 모드\"]])\n",
"plt.xticks(range(len(df)), [f\"case{i + 1}\" for i in range(len(df))])\n",
"plt.ylabel(\"ms per frame\"); plt.title(\"Stress test latency (red = degraded mode)\")\n",
"plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<details>\n",
"<summary><b>💡 [참고] 미션 08 예상 실행 결과 (Output)</b></summary>\n",
"\n",
"```text\n",
" 상황 경보 수신 유실률 다운 프레임당 ms 성능저하 모드\n",
" 평시(비행체 1~5기) 1 1 0.0 0 0.89 -\n",
" 포화 드론 40기 57 57 0.0 0 4.31 경보 57건 > 검토용량 30\n",
" 포화 드론 80기 97 97 0.0 0 9.48 경보 97건 > 검토용량 30\n",
" 포화 드론 120기 100 100 0.0 0 10.27 경보 100건 > 검토용량 30\n",
" 포화 120기 + 비 96 96 0.0 0 11.60 경보 96건 > 검토용량 30\n",
"포화 120기 + 프레임 10% 손실 97 97 0.0 0 8.17 프레임 손실 10%; 경보 97건 > 검토용량 30\n",
" 포화 120기 + 패킷 20% 손실 96 96 0.0 0 10.51 경보 96건 > 검토용량 30\n",
" 전 프레임 손실(센서 두절) 0 0 0.0 0 0.00 프레임 손실 100%\n",
"\n",
"시스템 다운 0건, 최대 유실률 0%, 최대 프레임당 처리 11.6 ms\n",
"관찰: 64x64 영상에 120기가 겹치면 표적끼리 붙어 경보 수가 실제보다 적게 잡힌다(해상도 한계). 성능저하 모드가 표시되면 운용자 추가 투입·검토 우선순위 적용이 필요하다.\n",
"```\n",
"</details>\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## 🎯 미션 09. AAR 사후검토 기반 미탐·오탐 공중표적 오류 원인 분석 및 파라미터 재보정\n",
"**소요시간**: 약 3.0시간\n",
"\n",
"### 📋 미션 배경 및 수행 지침\n",
"#### 미션 배경 및 당위성\n",
"\n",
"AAR 사후검토 일지를 기반으로 모의 교전 중 발생한 미탐 및 오경보 사례의 근본 원인을 분석하고 탐지 임계값과 가중치 파라미터를 역환류(Feedback) 보정한다. 실제 전술 현장에서는 단순 탐지에 그치지 않고 신속한 위협 판단과 신뢰도 높은 의사결정이 필수적이므로, **AAR 사후검토 일지로 미탐·오경보의 근본 원인을 분석하고 탐지 임계값과 경보 임계값을 역환류 보정한다.** 과업을 완수하여 오판을 방지하고 작전 대응력을 극대화해야 합니다.\n",
"\n",
"이 문제를 해결하기 위해 본 미션에서는 **'놓치지 않기'를 우선한 초기 설정으로 모의 운용한 뒤, 오경보는 실제 정체·기상별로, 미탐은 추적 실패·점수 미달로 분류한 AAR 일지 작성하여 원인을 분석하는** 실습을 체계적으로 수행합니다.\n",
"\n",
"학습자는 실습가이드에 제시된 총 4개 세부 활동(기본 기능 구현 ➔ 결과 지표 검증 ➔ 파라미터 튜닝 및 도전과제)을 순차적으로 진행하며 핵심 판정 기준을 달성합니다.\n",
"\n",
"**이번 결과물:** `docs/ops01_aar_report.md 및 configs/ops01_tuned_params.yaml` 형태의 결과물을 완성한다.\n",
"\n",
"### 🔍 평가 및 검증 기준 (PASS 조건)\n",
"#### 미션 판정\n",
"\n",
"**평가 구분:** 필수\n",
"\n",
"**핵심 평가영역:** 오류 원인 규명의 객관성 및 개선 조치의 논리적 타당성\n",
"\n",
"#### 미션 PASS 조건\n",
"\n",
"- 정량 기준: AAR 피드백 적용 후 종합 정확도 10%p 이상 향상, 재발 오류 0건\n",
"\n",
"\n",
"- 진행 기준: AAR 보고서 작성 및 파라미터 튜닝 전후 회귀 테스트 통과\n",
"\n",
"\n",
"- 검증 방법: 회귀 테스트(Regression Test) 스위트 전체 자동 실행 및 비교\n",
"\n",
"\n",
"#### 채점자 판정 메모\n",
"\n",
"> 단순 코드 실행 여부뿐만 아니라 출력된 수치와 근거 자료가 군 실무 요구사항에 부합하는지 면밀히 확인한다.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"AAR(사후검토) 일지 형식으로 모의 운용 중 발생한 미탐·오경보 사례를 원인별로 분류하고, 탐지 임계값 k와 경보 임계값을 격자 탐색으로 재보정(역환류)해 개선 전후 성능과 재발 여부를 비교한다.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy import ndimage\n",
"from sklearn.linear_model import LogisticRegression\n",
"from sklearn.pipeline import make_pipeline\n",
"from sklearn.preprocessing import StandardScaler\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [교육용가상자료] 합성 적외선 영상 생성기 (실제 영상·부대 정보 없음)\n",
"# 64x64 프레임에 드론·조류·민간기·지상 반사체(클러터)를 그리고 정답 라벨을 함께 만든다.\n",
"# ---------------------------------------------------------------------------\n",
"H = W = 64\n",
"PROFILE = {\"drone\": ((2, 3), (170, 225)), \"bird\": ((2, 3), (110, 150)),\n",
" \"civil\": ((4, 6), (190, 240)), \"clutter\": ((2, 4), (140, 190))}\n",
"\n",
"\n",
"def _new_obj(cls, rng, dive=False, v=None, xy=None):\n",
" # [TODO : 학생 실습 9] AAR 사후검토 기반 미탐·오탐 공중표적 오류 원인 분석 및 파라미터 재보정 관련 핵심 로직을 구현하세요.\n",
" # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.\n",
" # <<< 여기에 코드를 작성하세요 >>>\n",
" pass # 작성 후 pass를 제거하세요.\n",
"def _noise(img, cond, rng):\n",
" # [TODO : 학생 실습 9] AAR 사후검토 기반 미탐·오탐 공중표적 오류 원인 분석 및 파라미터 재보정 관련 핵심 로직을 구현하세요.\n",
" # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.\n",
" # <<< 여기에 코드를 작성하세요 >>>\n",
" pass # 작성 후 pass를 제거하세요.\n",
"def make_sequence(seq_id, cond, rng, kind=\"mixed\", n_frames=30, n_drone=None, n_bird=None):\n",
" \"\"\"시퀀스 1개 = (프레임 배열, 라벨 목록). kind: mixed / swarm / dive / saturation\"\"\"\n",
" objs = []\n",
" if kind == \"swarm\":\n",
" v, cx, cy = rng.uniform(-1, 1, 2), rng.uniform(15, 35), rng.uniform(15, 35)\n",
" objs = [_new_obj(\"drone\", rng, v=v, xy=(cx + i * 5, cy + (i % 2) * 5)) for i in range(4)]\n",
" objs.append(_new_obj(\"bird\", rng))\n",
" elif kind == \"dive\":\n",
" d = _new_obj(\"drone\", rng, dive=True)\n",
" d[\"y\"] = float(rng.uniform(4, 14))\n",
" objs = [d, _new_obj(\"bird\", rng), _new_obj(\"clutter\", rng)]\n",
" elif kind == \"saturation\":\n",
" objs = [_new_obj(\"drone\", rng) for _ in range(n_drone)] + [_new_obj(\"bird\", rng) for _ in range(n_bird)]\n",
" else:\n",
" objs = [_new_obj(\"drone\", rng) for _ in range(int(rng.integers(1, 3)))]\n",
" objs += [_new_obj(\"bird\", rng) for _ in range(int(rng.integers(0, 3)))]\n",
" if rng.random() < 0.5:\n",
" objs.append(_new_obj(\"civil\", rng))\n",
" if rng.random() < 0.6:\n",
" objs.append(_new_obj(\"clutter\", rng))\n",
" frames, labels = np.zeros((n_frames, H, W), np.uint8), []\n",
" base = rng.normal(40, 6, (H, W))\n",
" for t in range(n_frames):\n",
" canvas = base.copy()\n",
" for i, o in enumerate(objs):\n",
" if t > 0:\n",
" if o[\"cls\"] == \"bird\":\n",
" o[\"vx\"] = float(np.clip(o[\"vx\"] + rng.normal(0, 0.45), -1.6, 1.6))\n",
" o[\"vy\"] = float(np.clip(o[\"vy\"] + rng.normal(0, 0.45), -1.6, 1.6))\n",
" if o[\"dive\"] and t > 10:\n",
" o[\"vy\"] += 0.18\n",
" o[\"x\"] = float(np.clip(o[\"x\"] + o[\"vx\"], 1, W - o[\"size\"] - 1))\n",
" o[\"y\"] = float(np.clip(o[\"y\"] + o[\"vy\"], 1, H - o[\"size\"] - 1))\n",
" heat = o[\"heat\"]\n",
" if o[\"cls\"] == \"clutter\" and rng.random() < 0.3:\n",
" heat *= 0.7\n",
" if o[\"cls\"] == \"bird\":\n",
" heat *= float(rng.uniform(0.85, 1.1))\n",
" x, y, s = int(round(o[\"x\"])), int(round(o[\"y\"])), o[\"size\"]\n",
" canvas[y:y + s, x:x + s] = np.maximum(canvas[y:y + s, x:x + s], heat)\n",
" labels.append({\"frame\": t, \"track\": f\"{seq_id}_T{i:02d}\", \"class\": o[\"cls\"], \"bbox\": [x, y, s, s],\n",
" \"pattern\": \"dive\" if o[\"dive\"] else (\"swarm\" if kind == \"swarm\" and o[\"cls\"] == \"drone\" else \"normal\")})\n",
" frames[t] = _noise(canvas, cond, rng)\n",
" return frames, labels\n",
"\n",
"\n",
"def make_dataset(n=24, seed=7):\n",
" rng = np.random.default_rng(seed)\n",
" conds = [\"clear\", \"night\", \"fog\", \"rain\"]\n",
" out = []\n",
" for i in range(n):\n",
" kind = \"swarm\" if i % 8 == 5 else (\"dive\" if i % 8 == 6 else \"mixed\")\n",
" f, lab = make_sequence(f\"SEQ{i:03d}\", conds[i % 4], rng, kind)\n",
" out.append({\"id\": f\"SEQ{i:03d}\", \"cond\": conds[i % 4], \"kind\": kind, \"frames\": f, \"labels\": lab})\n",
" return out\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- 전처리·탐지 공통 함수 ----\n",
"def preprocess(frame, cond):\n",
" f = frame.astype(float)\n",
" if cond == \"rain\": # 폭 1px 세로 빗줄기 제거(가로 방향 형태학적 열림)\n",
" f = ndimage.grey_opening(f, size=(1, 2))\n",
" lo, hi = np.percentile(f, [1, 99.5]) # 대비 확장\n",
" return np.clip((f - lo) / (hi - lo + 1e-6) * 255, 0, 255)\n",
"\n",
"\n",
"def detect(frame, cond, k=2.0, min_area=3, max_area=60):\n",
" img = preprocess(frame, cond)\n",
" lab, _ = ndimage.label(img > img.mean() + k * img.std())\n",
" dets = []\n",
" for i, sl in enumerate(ndimage.find_objects(lab), start=1):\n",
" area = int((lab[sl] == i).sum())\n",
" if min_area <= area <= max_area:\n",
" y, x = sl[0].start, sl[1].start\n",
" dets.append({\"bbox\": [x, y, sl[1].stop - x, sl[0].stop - y], \"area\": area,\n",
" \"thermal\": float(frame[sl][lab[sl] == i].max())})\n",
" return dets\n",
"\n",
"\n",
"def iou(a, b):\n",
" ix = max(0, min(a[0] + a[2], b[0] + b[2]) - max(a[0], b[0]))\n",
" iy = max(0, min(a[1] + a[3], b[1] + b[3]) - max(a[1], b[1]))\n",
" inter = ix * iy\n",
" return inter / (a[2] * a[3] + b[2] * b[3] - inter)\n",
"\n",
"# ---- 추적·특징·패턴 공통 함수 ----\n",
"def center(b):\n",
" return np.array([b[0] + b[2] / 2, b[1] + b[3] / 2])\n",
"\n",
"\n",
"def track(per_frame, gate=6.0, max_missed=2, min_len=5):\n",
" \"\"\"예측 위치(마지막 위치 + 속도)와 가장 가까운 탐지를 이어 궤적을 만든다.\"\"\"\n",
" active, done = [], []\n",
" for t, dets in enumerate(per_frame):\n",
" used = set()\n",
" for tr in sorted(active, key=lambda x: -len(x[\"t\"])):\n",
" last = center(tr[\"b\"][-1])\n",
" vel = last - center(tr[\"b\"][-2]) if len(tr[\"b\"]) > 1 else np.zeros(2)\n",
" pred = last + vel * (t - tr[\"t\"][-1])\n",
" cand = [(np.linalg.norm(center(d[\"bbox\"]) - pred), j) for j, d in enumerate(dets) if j not in used]\n",
" dist, j = min(cand, default=(gate, -1))\n",
" if j >= 0 and dist < gate:\n",
" used.add(j)\n",
" tr[\"t\"].append(t); tr[\"b\"].append(dets[j][\"bbox\"])\n",
" tr[\"area\"].append(dets[j][\"area\"]); tr[\"heat\"].append(dets[j][\"thermal\"])\n",
" still = []\n",
" for tr in active:\n",
" (still if t - tr[\"t\"][-1] <= max_missed else done).append(tr)\n",
" active = still\n",
" for j, d in enumerate(dets):\n",
" if j not in used:\n",
" active.append({\"t\": [t], \"b\": [d[\"bbox\"]], \"area\": [d[\"area\"]], \"heat\": [d[\"thermal\"]]})\n",
" return [tr for tr in done + active if len(tr[\"t\"]) >= min_len]\n",
"\n",
"\n",
"FEATURES = [\"mean_speed\", \"speed_std\", \"turn\", \"straight\", \"vy_accel\", \"area\", \"heat\", \"heat_std\"]\n",
"\n",
"\n",
"def features(tr):\n",
" c = np.array([center(b) for b in tr[\"b\"]]); f = np.array(tr[\"t\"], float)\n",
" v = np.diff(c, axis=0) / np.maximum(np.diff(f), 1)[:, None]\n",
" sp = np.linalg.norm(v, axis=1)\n",
" k = 3 if len(c) > 4 else 1\n",
" v3 = (c[k:] - c[:-k]) / np.maximum(f[k:] - f[:-k], 1)[:, None]\n",
" hd = np.arctan2(v3[:, 1], v3[:, 0]); mv = np.linalg.norm(v3, axis=1) > 0.2\n",
" turns = np.abs(np.angle(np.exp(1j * np.diff(hd))))[mv[1:] & mv[:-1]]\n",
" half = len(v) // 2\n",
" return {\"mean_speed\": sp.mean(), \"speed_std\": sp.std(), \"turn\": turns.mean() if len(turns) else 0.0,\n",
" \"straight\": np.linalg.norm(c[-1] - c[0]) / max(sp.sum(), 1e-6),\n",
" \"vy_accel\": (v[half:, 1].mean() - v[:half, 1].mean()) / max(1, (f[-1] - f[0]) / 2) if half else 0.0,\n",
" \"area\": np.mean(tr[\"area\"]), \"heat\": np.mean(tr[\"heat\"]), \"heat_std\": np.std(tr[\"heat\"]),\n",
" \"vx\": v[:, 0].mean(), \"vy\": v[:, 1].mean(), \"x\": c[-1, 0], \"y\": c[-1, 1]}\n",
"\n",
"\n",
"def pattern(f):\n",
" if f[\"mean_speed\"] < 0.25:\n",
" return \"static\"\n",
" if f[\"vy_accel\"] > 0.06:\n",
" return \"dive\"\n",
" if f[\"turn\"] > 0.4 or f[\"straight\"] < 0.6:\n",
" return \"erratic\"\n",
" return \"steady\"\n",
"\n",
"\n",
"def swarms(trs, vel=0.2, dist=18.0, min_n=3):\n",
" \"\"\"속도가 비슷하고 가까이 움직이는 궤적 3개 이상을 군집으로 묶는다.\"\"\"\n",
" cand = [i for i, t in enumerate(trs) if t[\"pattern\"] != \"static\"]\n",
" adj = {i: set() for i in cand}\n",
" for a in cand:\n",
" for b in cand:\n",
" fa, fb = trs[a][\"f\"], trs[b][\"f\"]\n",
" if a < b and np.hypot(fa[\"vx\"] - fb[\"vx\"], fa[\"vy\"] - fb[\"vy\"]) < vel and np.hypot(fa[\"x\"] - fb[\"x\"], fa[\"y\"] - fb[\"y\"]) < dist:\n",
" adj[a].add(b); adj[b].add(a)\n",
" seen, groups = set(), []\n",
" for i in cand:\n",
" if i in seen:\n",
" continue\n",
" stack, comp = [i], []\n",
" while stack:\n",
" k = stack.pop()\n",
" if k not in seen:\n",
" seen.add(k); comp.append(k); stack += list(adj[k] - seen)\n",
" if len(comp) >= min_n:\n",
" groups.append(comp)\n",
" return groups\n",
"\n",
"\n",
"def analyze(s, k=2.0):\n",
" \"\"\"시퀀스 1개: 탐지 -> 추적 -> 특징 -> 패턴 -> 군집 -> 정답 라벨 연결(평가용).\"\"\"\n",
" per = [detect(fr, s[\"cond\"], k) if fr is not None else [] for fr in s[\"frames\"]]\n",
" trs = track(per)\n",
" for tr in trs:\n",
" tr[\"f\"] = features(tr); tr[\"pattern\"] = pattern(tr[\"f\"]); tr[\"swarm\"] = None; tr[\"seq\"] = s[\"id\"]\n",
" votes = {}\n",
" for t, b in zip(tr[\"t\"], tr[\"b\"]):\n",
" best = max(((iou(b, l[\"bbox\"]), l) for l in s[\"labels\"] if l[\"frame\"] == t), default=(0, None), key=lambda x: x[0])\n",
" key = (best[1][\"track\"], best[1][\"class\"], best[1][\"pattern\"]) if best[0] > 0.05 else (\"none\", \"noise\", \"normal\")\n",
" votes[key] = votes.get(key, 0) + 1\n",
" tr[\"gt_track\"], tr[\"gt_class\"], tr[\"gt_pattern\"] = max(votes, key=votes.get)\n",
" for g, comp in enumerate(swarms(trs)):\n",
" for i in comp:\n",
" trs[i][\"swarm\"] = g\n",
" if trs[i][\"pattern\"] in (\"steady\", \"erratic\"):\n",
" trs[i][\"pattern\"] = \"swarm\"\n",
" return trs\n",
"\n",
"\n",
"def X_of(trs):\n",
" return np.array([[t[\"f\"][k] for k in FEATURES] for t in trs])\n",
"\n",
"\n",
"def run_system(k, train_seed=7, test_seed=11):\n",
" \"\"\"탐지 임계값 k로 학습·검증 데이터를 처리하고, 검증 궤적마다 드론 확률(prob)을 붙인다.\"\"\"\n",
" tr = [t for s in make_dataset(24, train_seed) for t in analyze(s, k)]\n",
" te_data = make_dataset(24, test_seed)\n",
" te = [t for s in te_data for t in analyze(s, k)]\n",
" m = make_pipeline(StandardScaler(), LogisticRegression(class_weight=\"balanced\", max_iter=1000, random_state=0))\n",
" m.fit(X_of(tr), [t[\"gt_class\"] == \"drone\" for t in tr])\n",
" for t, p in zip(te, m.predict_proba(X_of(te))[:, 1]):\n",
" t[\"prob\"] = p\n",
" t[\"cond\"] = next(s[\"cond\"] for s in te_data if s[\"id\"] == t[\"seq\"])\n",
" drones = {l[\"track\"] for s in te_data for l in s[\"labels\"] if l[\"class\"] == \"drone\"}\n",
" return te, drones\n",
"\n",
"\n",
"def set_alerts(te, thr):\n",
" for t in te:\n",
" t[\"alert\"] = t[\"pattern\"] != \"static\" and t[\"prob\"] >= thr\n",
" return te\n",
"\n",
"\n",
"def summarize(te, drones):\n",
" alerts = [t for t in te if t[\"alert\"]]\n",
" tp = {t[\"gt_track\"] for t in alerts if t[\"gt_class\"] == \"drone\"}\n",
" far = sum(t[\"gt_class\"] != \"drone\" for t in alerts) / max(len(alerts), 1)\n",
" rec = len(tp) / len(drones)\n",
" acc = 2 * (1 - far) * rec / max((1 - far) + rec, 1e-9)\n",
" return {\"경보\": len(alerts), \"오경보율\": round(far, 3), \"재현율\": round(rec, 3), \"종합정확도(F1)\": round(acc, 3)}\n",
"\n",
"\n",
"def aar_log(te, drones):\n",
" \"\"\"AAR 일지: 오경보는 실제 정체·기상별로, 미탐은 원인별로 분류.\"\"\"\n",
" rows = []\n",
" for t in te:\n",
" if t[\"alert\"] and t[\"gt_class\"] != \"drone\":\n",
" rows.append({\"구분\": \"오경보\", \"원인\": f\"{t['gt_class']} 오인\", \"기상\": t[\"cond\"], \"궤적\": t[\"seq\"]})\n",
" tracked = {t[\"gt_track\"] for t in te if t[\"gt_class\"] == \"drone\"}\n",
" alerted = {t[\"gt_track\"] for t in te if t[\"gt_class\"] == \"drone\" and t[\"alert\"]}\n",
" for d in sorted(drones):\n",
" if d not in tracked:\n",
" rows.append({\"구분\": \"미탐\", \"원인\": \"추적 실패(탐지 누락)\", \"기상\": \"-\", \"궤적\": d})\n",
" elif d not in alerted:\n",
" rows.append({\"구분\": \"미탐\", \"원인\": \"신뢰도 점수 미달\", \"기상\": \"-\", \"궤적\": d})\n",
" return pd.DataFrame(rows, columns=[\"구분\", \"원인\", \"기상\", \"궤적\"])\n",
"\n",
"\n",
"# 1) 초기 설정(k=2.0, 경보 임계값 0.3: '놓치지 않기'를 우선한 설정)으로 모의 운용\n",
"before_te, drones = run_system(2.0)\n",
"set_alerts(before_te, 0.3)\n",
"before = summarize(before_te, drones)\n",
"log = aar_log(before_te, drones)\n",
"print(\"[1. 무엇이 일어났나] 초기 설정 결과:\", before)\n",
"print(log.groupby([\"구분\", \"원인\"]).size().rename(\"건수\").to_string())\n",
"print(\"\\n[2. 왜 일어났나] 오경보 기상 분포:\", log[log[\"구분\"] == \"오경보\"][\"기상\"].value_counts().to_dict())\n",
"\n",
"# 2) 재보정: 제약(오경보율 15% 이하, 재현율 90% 이상) 안에서 F1 최대 조합 탐색\n",
"grid = []\n",
"for k in [2.0, 2.5, 3.0]:\n",
" te, dr = run_system(k)\n",
" for thr in [0.3, 0.4, 0.5, 0.6, 0.7]:\n",
" grid.append({\"k\": k, \"경보임계값\": thr, **summarize(set_alerts(te, thr), dr)})\n",
"g = pd.DataFrame(grid)\n",
"ok = g[(g[\"오경보율\"] <= 0.15) & (g[\"재현율\"] >= 0.9)].sort_values([\"종합정확도(F1)\", \"재현율\"], ascending=False)\n",
"best = ok.iloc[0]\n",
"print(\"\\n[3. 무엇을 바꾸나] 격자 탐색 상위 5개\")\n",
"print(ok.head().to_string(index=False))\n",
"after_te, _ = run_system(best[\"k\"])\n",
"set_alerts(after_te, best[\"경보임계값\"])\n",
"after = summarize(after_te, drones)\n",
"after_log = aar_log(after_te, drones)\n",
"print(f\"\\n재보정값: k={best['k']}, 경보 임계값={best['경보임계값']}\")\n",
"print(\"재보정 후:\", after)\n",
"print(f\"종합정확도 변화: {before['종합정확도(F1)']:.3f} -> {after['종합정확도(F1)']:.3f} ({(after['종합정확도(F1)'] - before['종합정확도(F1)']) * 100:+.1f}%p)\")\n",
"repeat = set(log[log[\"구분\"] == \"오경보\"][\"궤적\"]) & set(after_log[after_log[\"구분\"] == \"오경보\"][\"궤적\"])\n",
"print(f\"재보정 후에도 반복된 오경보 시퀀스: {sorted(repeat) if repeat else '없음'}\")\n",
"\n",
"tuned = f\"detection_k: {best['k']}\\nalert_threshold: {best['경보임계값']}\\nconstraints: {{max_far: 0.15, min_recall: 0.9}}\"\n",
"print(\"\\n[재보정 파라미터(ops01_tuned_params.yaml 내용)]\\n\" + tuned)\n",
"\n",
"plt.figure(figsize=(6, 4))\n",
"for k in sorted(g[\"k\"].unique()):\n",
" s = g[g[\"k\"] == k]\n",
" plt.plot(s[\"오경보율\"], s[\"재현율\"], \"o-\", label=f\"k={k}\")\n",
"plt.axvline(0.15, ls=\"--\", c=\"gray\"); plt.axhline(0.9, ls=\"--\", c=\"gray\")\n",
"plt.xlabel(\"false alarm rate\"); plt.ylabel(\"drone recall\"); plt.legend(); plt.title(\"AAR re-tuning grid (synthetic)\")\n",
"plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<details>\n",
"<summary><b>💡 [참고] 미션 09 예상 실행 결과 (Output)</b></summary>\n",
"\n",
"```text\n",
"[1. 무엇이 일어났나] 초기 설정 결과: {'경보': 49, '오경보율': 0.102, '재현율': 1.0, '종합정확도(F1)': 0.946}\n",
"구분 원인 \n",
"오경보 bird 오인 4\n",
" civil 오인 1\n",
"\n",
"[2. 왜 일어났나] 오경보 기상 분포: {'clear': 2, 'night': 2, 'fog': 1}\n",
"\n",
"[3. 무엇을 바꾸나] 격자 탐색 상위 5개\n",
" k 경보임계값 경보 오경보율 재현율 종합정확도(F1)\n",
"2.5 0.5 44 0.023 0.977 0.977\n",
"2.5 0.6 44 0.023 0.977 0.977\n",
"2.0 0.5 45 0.044 0.977 0.966\n",
"2.5 0.4 45 0.044 0.977 0.966\n",
"2.0 0.4 48 0.083 1.000 0.957\n",
"\n",
"재보정값: k=2.5, 경보 임계값=0.5\n",
"재보정 후: {'경보': 44, '오경보율': 0.023, '재현율': 0.977, '종합정확도(F1)': 0.977}\n",
"종합정확도 변화: 0.946 -> 0.977 (+3.1%p)\n",
"재보정 후에도 반복된 오경보 시퀀스: ['SEQ022']\n",
"\n",
"[재보정 파라미터(ops01_tuned_params.yaml 내용)]\n",
"detection_k: 2.5\n",
"alert_threshold: 0.5\n",
"constraints: {max_far: 0.15, min_recall: 0.9}\n",
"```\n",
"</details>\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## 🎯 미션 10. 드론 공중위협 객체 탐지 최종 파이프라인 통합 및 대드론 방어 브리핑 패키지 완성\n",
"**소요시간**: 약 3.5시간\n",
"\n",
"### 📋 미션 배경 및 수행 지침\n",
"#### 미션 배경 및 당위성\n",
"\n",
"데이터 수집부터 전처리, 탐지 모델, UI, HITL, AAR 피드백까지 전체 파이프라인을 통합 빌드하고 야전 방공부대 배포 매뉴얼 및 지휘관 브리핑 패키지를 완성한다. 실제 전술 현장에서는 단순 탐지에 그치지 않고 신속한 위협 판단과 신뢰도 높은 의사결정이 필수적이므로, **데이터 생성부터 전처리, 탐지, 상황판, HITL, AAR 재보정값 적용까지 전체 파이프라인을 통합하고 지휘관 브리핑 패키지를 완성한다.** 과업을 완수하여 오판을 방지하고 작전 대응력을 극대화해야 합니다.\n",
"\n",
"이 문제를 해결하기 위해 본 미션에서는 **재보정 파라미터로 전체 과정을 한 번에 실행하는 통합 코드를 완성하고 손상 데이터가 자동 차단되는지 확인하고 유효성을 점검하는** 실습을 체계적으로 수행합니다.\n",
"\n",
"학습자는 실습가이드에 제시된 총 4개 세부 활동(기본 기능 구현 ➔ 결과 지표 검증 ➔ 파라미터 튜닝 및 도전과제)을 순차적으로 진행하며 핵심 판정 기준을 달성합니다.\n",
"\n",
"**이번 결과물:** `src/main_ops01.py, README.md, docs/ops01_final_report.pdf(md)` 형태의 결과물을 완성한다.\n",
"\n",
"### 🔍 평가 및 검증 기준 (PASS 조건)\n",
"#### 미션 판정\n",
"\n",
"**평가 구분:** 필수\n",
"\n",
"**핵심 평가영역:** 군 실무 즉시 도입 가능성, 운용 신뢰도 및 전략적 기대효과\n",
"\n",
"#### 미션 PASS 조건\n",
"\n",
"- 정량 기준: End-to-End 통합 파이프라인 실행 성공률 100%, 테스트 커버리지 85% 이상\n",
"\n",
"\n",
"- 진행 기준: 원클릭 실행 스크립트 동작 확인 및 최종 발표 산출물 작성 완료\n",
"\n",
"\n",
"- 검증 방법: 최종 검증 위원회(평가관) 모의 시연 및 전체 기능 통합 검증\n",
"\n",
"\n",
"#### 채점자 판정 메모\n",
"\n",
"> 단순 코드 실행 여부뿐만 아니라 출력된 수치와 근거 자료가 군 실무 요구사항에 부합하는지 면밀히 확인한다.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"데이터 생성·검증부터 전처리, 탐지, 추적·패턴, 신뢰도 점수, HITL 승인 대기열, 상황판, AAR 재보정 파라미터 적용까지 전체 파이프라인을 한 파일로 통합 실행하고, 자체 점검 결과와 지휘관 브리핑 요약을 출력한다.\"\"\"\n",
"import hashlib\n",
"import json\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy import ndimage\n",
"from sklearn.linear_model import LogisticRegression\n",
"from sklearn.pipeline import make_pipeline\n",
"from sklearn.preprocessing import StandardScaler\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [교육용가상자료] 합성 적외선 영상 생성기 (실제 영상·부대 정보 없음)\n",
"# 64x64 프레임에 드론·조류·민간기·지상 반사체(클러터)를 그리고 정답 라벨을 함께 만든다.\n",
"# ---------------------------------------------------------------------------\n",
"H = W = 64\n",
"PROFILE = {\"drone\": ((2, 3), (170, 225)), \"bird\": ((2, 3), (110, 150)),\n",
" \"civil\": ((4, 6), (190, 240)), \"clutter\": ((2, 4), (140, 190))}\n",
"\n",
"\n",
"def _new_obj(cls, rng, dive=False, v=None, xy=None):\n",
" # [TODO : 학생 실습 10] 드론 공중위협 객체 탐지 최종 파이프라인 통합 및 대드론 방어 브리핑 패키지 완성 관련 핵심 로직을 구현하세요.\n",
" # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.\n",
" # <<< 여기에 코드를 작성하세요 >>>\n",
" pass # 작성 후 pass를 제거하세요.\n",
"def _noise(img, cond, rng):\n",
" # [TODO : 학생 실습 10] 드론 공중위협 객체 탐지 최종 파이프라인 통합 및 대드론 방어 브리핑 패키지 완성 관련 핵심 로직을 구현하세요.\n",
" # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.\n",
" # <<< 여기에 코드를 작성하세요 >>>\n",
" pass # 작성 후 pass를 제거하세요.\n",
"def make_sequence(seq_id, cond, rng, kind=\"mixed\", n_frames=30, n_drone=None, n_bird=None):\n",
" \"\"\"시퀀스 1개 = (프레임 배열, 라벨 목록). kind: mixed / swarm / dive / saturation\"\"\"\n",
" objs = []\n",
" if kind == \"swarm\":\n",
" v, cx, cy = rng.uniform(-1, 1, 2), rng.uniform(15, 35), rng.uniform(15, 35)\n",
" objs = [_new_obj(\"drone\", rng, v=v, xy=(cx + i * 5, cy + (i % 2) * 5)) for i in range(4)]\n",
" objs.append(_new_obj(\"bird\", rng))\n",
" elif kind == \"dive\":\n",
" d = _new_obj(\"drone\", rng, dive=True)\n",
" d[\"y\"] = float(rng.uniform(4, 14))\n",
" objs = [d, _new_obj(\"bird\", rng), _new_obj(\"clutter\", rng)]\n",
" elif kind == \"saturation\":\n",
" objs = [_new_obj(\"drone\", rng) for _ in range(n_drone)] + [_new_obj(\"bird\", rng) for _ in range(n_bird)]\n",
" else:\n",
" objs = [_new_obj(\"drone\", rng) for _ in range(int(rng.integers(1, 3)))]\n",
" objs += [_new_obj(\"bird\", rng) for _ in range(int(rng.integers(0, 3)))]\n",
" if rng.random() < 0.5:\n",
" objs.append(_new_obj(\"civil\", rng))\n",
" if rng.random() < 0.6:\n",
" objs.append(_new_obj(\"clutter\", rng))\n",
" frames, labels = np.zeros((n_frames, H, W), np.uint8), []\n",
" base = rng.normal(40, 6, (H, W))\n",
" for t in range(n_frames):\n",
" canvas = base.copy()\n",
" for i, o in enumerate(objs):\n",
" if t > 0:\n",
" if o[\"cls\"] == \"bird\":\n",
" o[\"vx\"] = float(np.clip(o[\"vx\"] + rng.normal(0, 0.45), -1.6, 1.6))\n",
" o[\"vy\"] = float(np.clip(o[\"vy\"] + rng.normal(0, 0.45), -1.6, 1.6))\n",
" if o[\"dive\"] and t > 10:\n",
" o[\"vy\"] += 0.18\n",
" o[\"x\"] = float(np.clip(o[\"x\"] + o[\"vx\"], 1, W - o[\"size\"] - 1))\n",
" o[\"y\"] = float(np.clip(o[\"y\"] + o[\"vy\"], 1, H - o[\"size\"] - 1))\n",
" heat = o[\"heat\"]\n",
" if o[\"cls\"] == \"clutter\" and rng.random() < 0.3:\n",
" heat *= 0.7\n",
" if o[\"cls\"] == \"bird\":\n",
" heat *= float(rng.uniform(0.85, 1.1))\n",
" x, y, s = int(round(o[\"x\"])), int(round(o[\"y\"])), o[\"size\"]\n",
" canvas[y:y + s, x:x + s] = np.maximum(canvas[y:y + s, x:x + s], heat)\n",
" labels.append({\"frame\": t, \"track\": f\"{seq_id}_T{i:02d}\", \"class\": o[\"cls\"], \"bbox\": [x, y, s, s],\n",
" \"pattern\": \"dive\" if o[\"dive\"] else (\"swarm\" if kind == \"swarm\" and o[\"cls\"] == \"drone\" else \"normal\")})\n",
" frames[t] = _noise(canvas, cond, rng)\n",
" return frames, labels\n",
"\n",
"\n",
"def make_dataset(n=24, seed=7):\n",
" rng = np.random.default_rng(seed)\n",
" conds = [\"clear\", \"night\", \"fog\", \"rain\"]\n",
" out = []\n",
" for i in range(n):\n",
" kind = \"swarm\" if i % 8 == 5 else (\"dive\" if i % 8 == 6 else \"mixed\")\n",
" f, lab = make_sequence(f\"SEQ{i:03d}\", conds[i % 4], rng, kind)\n",
" out.append({\"id\": f\"SEQ{i:03d}\", \"cond\": conds[i % 4], \"kind\": kind, \"frames\": f, \"labels\": lab})\n",
" return out\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- 전처리·탐지 공통 함수 ----\n",
"def preprocess(frame, cond):\n",
" f = frame.astype(float)\n",
" if cond == \"rain\": # 폭 1px 세로 빗줄기 제거(가로 방향 형태학적 열림)\n",
" f = ndimage.grey_opening(f, size=(1, 2))\n",
" lo, hi = np.percentile(f, [1, 99.5]) # 대비 확장\n",
" return np.clip((f - lo) / (hi - lo + 1e-6) * 255, 0, 255)\n",
"\n",
"\n",
"def detect(frame, cond, k=2.0, min_area=3, max_area=60):\n",
" img = preprocess(frame, cond)\n",
" lab, _ = ndimage.label(img > img.mean() + k * img.std())\n",
" dets = []\n",
" for i, sl in enumerate(ndimage.find_objects(lab), start=1):\n",
" area = int((lab[sl] == i).sum())\n",
" if min_area <= area <= max_area:\n",
" y, x = sl[0].start, sl[1].start\n",
" dets.append({\"bbox\": [x, y, sl[1].stop - x, sl[0].stop - y], \"area\": area,\n",
" \"thermal\": float(frame[sl][lab[sl] == i].max())})\n",
" return dets\n",
"\n",
"\n",
"def iou(a, b):\n",
" ix = max(0, min(a[0] + a[2], b[0] + b[2]) - max(a[0], b[0]))\n",
" iy = max(0, min(a[1] + a[3], b[1] + b[3]) - max(a[1], b[1]))\n",
" inter = ix * iy\n",
" return inter / (a[2] * a[3] + b[2] * b[3] - inter)\n",
"\n",
"# ---- 추적·특징·패턴 공통 함수 ----\n",
"def center(b):\n",
" return np.array([b[0] + b[2] / 2, b[1] + b[3] / 2])\n",
"\n",
"\n",
"def track(per_frame, gate=6.0, max_missed=2, min_len=5):\n",
" \"\"\"예측 위치(마지막 위치 + 속도)와 가장 가까운 탐지를 이어 궤적을 만든다.\"\"\"\n",
" active, done = [], []\n",
" for t, dets in enumerate(per_frame):\n",
" used = set()\n",
" for tr in sorted(active, key=lambda x: -len(x[\"t\"])):\n",
" last = center(tr[\"b\"][-1])\n",
" vel = last - center(tr[\"b\"][-2]) if len(tr[\"b\"]) > 1 else np.zeros(2)\n",
" pred = last + vel * (t - tr[\"t\"][-1])\n",
" cand = [(np.linalg.norm(center(d[\"bbox\"]) - pred), j) for j, d in enumerate(dets) if j not in used]\n",
" dist, j = min(cand, default=(gate, -1))\n",
" if j >= 0 and dist < gate:\n",
" used.add(j)\n",
" tr[\"t\"].append(t); tr[\"b\"].append(dets[j][\"bbox\"])\n",
" tr[\"area\"].append(dets[j][\"area\"]); tr[\"heat\"].append(dets[j][\"thermal\"])\n",
" still = []\n",
" for tr in active:\n",
" (still if t - tr[\"t\"][-1] <= max_missed else done).append(tr)\n",
" active = still\n",
" for j, d in enumerate(dets):\n",
" if j not in used:\n",
" active.append({\"t\": [t], \"b\": [d[\"bbox\"]], \"area\": [d[\"area\"]], \"heat\": [d[\"thermal\"]]})\n",
" return [tr for tr in done + active if len(tr[\"t\"]) >= min_len]\n",
"\n",
"\n",
"FEATURES = [\"mean_speed\", \"speed_std\", \"turn\", \"straight\", \"vy_accel\", \"area\", \"heat\", \"heat_std\"]\n",
"\n",
"\n",
"def features(tr):\n",
" c = np.array([center(b) for b in tr[\"b\"]]); f = np.array(tr[\"t\"], float)\n",
" v = np.diff(c, axis=0) / np.maximum(np.diff(f), 1)[:, None]\n",
" sp = np.linalg.norm(v, axis=1)\n",
" k = 3 if len(c) > 4 else 1\n",
" v3 = (c[k:] - c[:-k]) / np.maximum(f[k:] - f[:-k], 1)[:, None]\n",
" hd = np.arctan2(v3[:, 1], v3[:, 0]); mv = np.linalg.norm(v3, axis=1) > 0.2\n",
" turns = np.abs(np.angle(np.exp(1j * np.diff(hd))))[mv[1:] & mv[:-1]]\n",
" half = len(v) // 2\n",
" return {\"mean_speed\": sp.mean(), \"speed_std\": sp.std(), \"turn\": turns.mean() if len(turns) else 0.0,\n",
" \"straight\": np.linalg.norm(c[-1] - c[0]) / max(sp.sum(), 1e-6),\n",
" \"vy_accel\": (v[half:, 1].mean() - v[:half, 1].mean()) / max(1, (f[-1] - f[0]) / 2) if half else 0.0,\n",
" \"area\": np.mean(tr[\"area\"]), \"heat\": np.mean(tr[\"heat\"]), \"heat_std\": np.std(tr[\"heat\"]),\n",
" \"vx\": v[:, 0].mean(), \"vy\": v[:, 1].mean(), \"x\": c[-1, 0], \"y\": c[-1, 1]}\n",
"\n",
"\n",
"def pattern(f):\n",
" if f[\"mean_speed\"] < 0.25:\n",
" return \"static\"\n",
" if f[\"vy_accel\"] > 0.06:\n",
" return \"dive\"\n",
" if f[\"turn\"] > 0.4 or f[\"straight\"] < 0.6:\n",
" return \"erratic\"\n",
" return \"steady\"\n",
"\n",
"\n",
"def swarms(trs, vel=0.2, dist=18.0, min_n=3):\n",
" \"\"\"속도가 비슷하고 가까이 움직이는 궤적 3개 이상을 군집으로 묶는다.\"\"\"\n",
" cand = [i for i, t in enumerate(trs) if t[\"pattern\"] != \"static\"]\n",
" adj = {i: set() for i in cand}\n",
" for a in cand:\n",
" for b in cand:\n",
" fa, fb = trs[a][\"f\"], trs[b][\"f\"]\n",
" if a < b and np.hypot(fa[\"vx\"] - fb[\"vx\"], fa[\"vy\"] - fb[\"vy\"]) < vel and np.hypot(fa[\"x\"] - fb[\"x\"], fa[\"y\"] - fb[\"y\"]) < dist:\n",
" adj[a].add(b); adj[b].add(a)\n",
" seen, groups = set(), []\n",
" for i in cand:\n",
" if i in seen:\n",
" continue\n",
" stack, comp = [i], []\n",
" while stack:\n",
" k = stack.pop()\n",
" if k not in seen:\n",
" seen.add(k); comp.append(k); stack += list(adj[k] - seen)\n",
" if len(comp) >= min_n:\n",
" groups.append(comp)\n",
" return groups\n",
"\n",
"\n",
"def analyze(s, k=2.0):\n",
" \"\"\"시퀀스 1개: 탐지 -> 추적 -> 특징 -> 패턴 -> 군집 -> 정답 라벨 연결(평가용).\"\"\"\n",
" per = [detect(fr, s[\"cond\"], k) if fr is not None else [] for fr in s[\"frames\"]]\n",
" trs = track(per)\n",
" for tr in trs:\n",
" tr[\"f\"] = features(tr); tr[\"pattern\"] = pattern(tr[\"f\"]); tr[\"swarm\"] = None; tr[\"seq\"] = s[\"id\"]\n",
" votes = {}\n",
" for t, b in zip(tr[\"t\"], tr[\"b\"]):\n",
" best = max(((iou(b, l[\"bbox\"]), l) for l in s[\"labels\"] if l[\"frame\"] == t), default=(0, None), key=lambda x: x[0])\n",
" key = (best[1][\"track\"], best[1][\"class\"], best[1][\"pattern\"]) if best[0] > 0.05 else (\"none\", \"noise\", \"normal\")\n",
" votes[key] = votes.get(key, 0) + 1\n",
" tr[\"gt_track\"], tr[\"gt_class\"], tr[\"gt_pattern\"] = max(votes, key=votes.get)\n",
" for g, comp in enumerate(swarms(trs)):\n",
" for i in comp:\n",
" trs[i][\"swarm\"] = g\n",
" if trs[i][\"pattern\"] in (\"steady\", \"erratic\"):\n",
" trs[i][\"pattern\"] = \"swarm\"\n",
" return trs\n",
"\n",
"CONFIG = {\"detection_k\": 2.5, \"alert_threshold\": 0.5, \"queue_capacity\": 30} # 미션 9 재보정값\n",
"\n",
"\n",
"def validate(seq):\n",
" errs = []\n",
" if seq[\"cond\"] not in (\"clear\", \"night\", \"fog\", \"rain\"):\n",
" errs.append(\"condition 허용값 아님\")\n",
" if seq[\"frames\"].ndim != 3 or seq[\"frames\"].shape[1:] != (64, 64):\n",
" errs.append(\"해상도 오류\")\n",
" for l in seq[\"labels\"]:\n",
" if l[\"class\"] not in PROFILE or len(l[\"bbox\"]) != 4:\n",
" errs.append(\"라벨 오류\"); break\n",
" return errs\n",
"\n",
"\n",
"def X_of(trs):\n",
" return np.array([[t[\"f\"][k] for k in FEATURES] for t in trs])\n",
"\n",
"\n",
"class Queue:\n",
" def __init__(self):\n",
" self.items, self.log = [], []\n",
"\n",
" def submit(self, t):\n",
" prev = self.log[-1][\"hash\"] if self.log else \"GENESIS\"\n",
" body = {\"event\": \"SUBMIT\", \"id\": f\"{t['seq']}-{len(self.items)}\", \"score\": round(t[\"score\"], 3)}\n",
" self.log.append({**body, \"prev\": prev, \"hash\": hashlib.sha256((prev + json.dumps(body, sort_keys=True)).encode()).hexdigest()})\n",
" self.items.append({**body, \"grade\": t[\"grade\"], \"status\": \"PENDING\"})\n",
"\n",
"\n",
"# ---------------- 1. 학습(엔진 준비) ----------------\n",
"t_start = time.perf_counter()\n",
"train = [t for s in make_dataset(24, 7) for t in analyze(s, CONFIG[\"detection_k\"])]\n",
"engine = make_pipeline(StandardScaler(), LogisticRegression(class_weight=\"balanced\", max_iter=1000, random_state=0))\n",
"engine.fit(X_of(train), [t[\"gt_class\"] == \"drone\" for t in train])\n",
"\n",
"# ---------------- 2. 운용 데이터 처리 ----------------\n",
"ops = make_dataset(24, 11)\n",
"ops[3][\"cond\"] = \"snow\" # 손상 데이터 1건 주입(검증 단계에서 걸러져야 함)\n",
"queue, rejected, all_tracks, runs = Queue(), {}, [], []\n",
"for s in ops:\n",
" errs = validate(s)\n",
" if errs:\n",
" rejected[s[\"id\"]] = errs; continue\n",
" t0 = time.perf_counter()\n",
" trs = analyze(s, CONFIG[\"detection_k\"])\n",
" if trs:\n",
" for t, p in zip(trs, engine.predict_proba(X_of(trs))[:, 1]):\n",
" t[\"score\"] = 0.0 if t[\"pattern\"] == \"static\" else float(p)\n",
" t[\"grade\"] = \"High\" if t[\"score\"] >= 0.8 else (\"Medium\" if t[\"score\"] >= 0.5 else \"Low\")\n",
" t[\"alert\"] = t[\"pattern\"] != \"static\" and t[\"score\"] >= CONFIG[\"alert_threshold\"]\n",
" if t[\"alert\"]:\n",
" queue.submit(t)\n",
" runs.append({\"seq\": s[\"id\"], \"ms_per_frame\": (time.perf_counter() - t0) * 1000 / len(s[\"frames\"]),\n",
" \"alerts\": sum(t[\"alert\"] for t in trs)})\n",
" all_tracks += trs\n",
"\n",
"# ---------------- 3. 성능 평가 ----------------\n",
"drones = {l[\"track\"] for s in ops if s[\"id\"] not in rejected for l in s[\"labels\"] if l[\"class\"] == \"drone\"}\n",
"alerts = [t for t in all_tracks if t[\"alert\"]]\n",
"far = sum(t[\"gt_class\"] != \"drone\" for t in alerts) / max(len(alerts), 1)\n",
"recall = len({t[\"gt_track\"] for t in alerts if t[\"gt_class\"] == \"drone\"}) / len(drones)\n",
"ms = np.mean([r[\"ms_per_frame\"] for r in runs])\n",
"\n",
"# ---------------- 4. 자체 점검(통합 테스트) ----------------\n",
"chk = {\n",
" \"손상 데이터 차단\": \"SEQ003\" in rejected,\n",
" \"처리 시퀀스 23개\": len(runs) == 23,\n",
" \"드론 재현율 90% 이상\": recall >= 0.9,\n",
" \"오경보율 15% 이하\": far <= 0.15,\n",
" \"자동 승인 0건\": all(i[\"status\"] == \"PENDING\" for i in queue.items),\n",
" \"경보 전량 대기열 등록\": len(queue.items) == len(alerts),\n",
" \"감사 로그 건수 일치\": len(queue.log) == len(queue.items),\n",
" \"프레임당 50ms 이내\": ms < 50,\n",
" \"등급 규칙 일관\": all((t[\"grade\"] == \"High\") == (t[\"score\"] >= 0.8) for t in all_tracks),\n",
" \"static 궤적 경보 없음\": not any(t[\"alert\"] and t[\"pattern\"] == \"static\" for t in all_tracks),\n",
"}\n",
"passed = sum(chk.values())\n",
"for k, v in chk.items():\n",
" print(f\"[{'PASS' if v else 'FAIL'}] {k}\")\n",
"print(f\"자체 점검 {passed}/{len(chk)} 통과 ({passed / len(chk):.0%}), 파이프라인 실행 성공 {'예' if passed == len(chk) else '아니오'}\")\n",
"\n",
"# ---------------- 5. 상황판 ----------------\n",
"focus = max({t[\"seq\"] for t in all_tracks}, key=lambda q: sum(t[\"alert\"] for t in all_tracks if t[\"seq\"] == q))\n",
"col = {\"High\": \"tab:red\", \"Medium\": \"tab:orange\", \"Low\": \"tab:gray\"}\n",
"fig, ax = plt.subplots(2, 2, figsize=(12, 8))\n",
"s = next(x for x in ops if x[\"id\"] == focus)\n",
"ax[0, 0].imshow(s[\"frames\"][-1], cmap=\"gray\")\n",
"for t in [t for t in all_tracks if t[\"seq\"] == focus]:\n",
" c = np.array([center(b) for b in t[\"b\"]]); ax[0, 0].plot(c[:, 0], c[:, 1], color=col[t[\"grade\"]], lw=2)\n",
"ax[0, 0].set_title(f\"{focus} tracks (color = grade)\"); ax[0, 0].axis(\"off\")\n",
"g = pd.Series([t[\"grade\"] for t in all_tracks]).value_counts().reindex([\"High\", \"Medium\", \"Low\"]).fillna(0)\n",
"ax[0, 1].bar(g.index, g.values, color=[col[i] for i in g.index]); ax[0, 1].set_title(\"Tracks by grade\")\n",
"ax[1, 0].bar([r[\"seq\"][-2:] for r in runs], [r[\"alerts\"] for r in runs]); ax[1, 0].set_title(\"Alerts per sequence (pending human review)\")\n",
"ax[1, 1].axis(\"off\")\n",
"ax[1, 1].text(0, 0.9, f\"Drone recall {recall:.1%}\\nFalse alarm {far:.1%}\\nQueue {len(queue.items)} pending\\n\"\n",
" f\"Latency {ms:.2f} ms/frame\\nSelf-check {passed}/{len(chk)}\", family=\"monospace\", fontsize=13, va=\"top\")\n",
"plt.suptitle(\"OPS01 integrated pipeline - training data only\"); plt.tight_layout(); plt.show()\n",
"\n",
"# ---------------- 6. 지휘관 브리핑 요약 ----------------\n",
"print(f\"\"\"\n",
"[대드론 방어 브리핑 요약 - 교육용가상자료]\n",
"1. 처리: 시퀀스 {len(ops)}개 중 {len(runs)}개 처리, 손상 데이터 {len(rejected)}건 차단({', '.join(rejected)})\n",
"2. 성능: 드론 재현율 {recall:.1%}, 오경보율 {far:.1%}, 프레임당 {ms:.2f} ms, 전체 소요 {time.perf_counter() - t_start:.1f} s\n",
"3. 경보: {len(queue.items)}건 운용자 검토 대기 (High {sum(i['grade'] == 'High' for i in queue.items)} / Medium {sum(i['grade'] == 'Medium' for i in queue.items)}), 자동 승인 0건\n",
"4. 운용 유의: 경보는 운용자 승인 후에만 상급 지휘계통으로 전파하며, 교전 여부는 교전수칙·지휘계통에 따른다.\n",
"5. 한계: 합성 64x64 영상 기준 결과로, 실제 센서·기상·배경에서는 재학습과 재검증이 필요하다.\"\"\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<details>\n",
"<summary><b>💡 [참고] 미션 10 예상 실행 결과 (Output)</b></summary>\n",
"\n",
"```text\n",
"[PASS] 손상 데이터 차단\n",
"[PASS] 처리 시퀀스 23개\n",
"[PASS] 드론 재현율 90% 이상\n",
"[PASS] 오경보율 15% 이하\n",
"[PASS] 자동 승인 0건\n",
"[PASS] 경보 전량 대기열 등록\n",
"[PASS] 감사 로그 건수 일치\n",
"[PASS] 프레임당 50ms 이내\n",
"[PASS] 등급 규칙 일관\n",
"[PASS] static 궤적 경보 없음\n",
"자체 점검 10/10 통과 (100%), 파이프라인 실행 성공 예\n",
"\n",
"[대드론 방어 브리핑 요약 - 교육용가상자료]\n",
"1. 처리: 시퀀스 24개 중 23개 처리, 손상 데이터 1건 차단(SEQ003)\n",
"2. 성능: 드론 재현율 97.6%, 오경보율 2.4%, 프레임당 0.67 ms, 전체 소요 1.5 s\n",
"3. 경보: 42건 운용자 검토 대기 (High 32 / Medium 10), 자동 승인 0건\n",
"4. 운용 유의: 경보는 운용자 승인 후에만 상급 지휘계통으로 전파하며, 교전 여부는 교전수칙·지휘계통에 따른다.\n",
"5. 한계: 합성 64x64 영상 기준 결과로, 실제 센서·기상·배경에서는 재학습과 재검증이 필요하다.\n",
"```\n",
"</details>\n"
]
}
],
"metadata": {
"colab": {
"provenance": []
},
"kernelspec": {
"display_name": "Python 3",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 0
}