{ "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", " if not isinstance(value, rule[\"type\"]) or isinstance(value, bool):\n", " return [f\"{name}: ์ž๋ฃŒํ˜• ์˜ค๋ฅ˜({type(value).__name__})\"]\n", " errs = []\n", " if \"enum\" in rule and value not in rule[\"enum\"]:\n", " errs.append(f\"{name}: ํ—ˆ์šฉ๋˜์ง€ ์•Š์€ ๊ฐ’ '{value}'\")\n", " if \"min\" in rule and value < rule[\"min\"]:\n", " errs.append(f\"{name}: ์ตœ์†Œ๊ฐ’ {rule['min']} ๋ฏธ๋งŒ\")\n", " if \"max\" in rule and value > rule[\"max\"]:\n", " errs.append(f\"{name}: ์ตœ๋Œ€๊ฐ’ {rule['max']} ์ดˆ๊ณผ\")\n", " if \"length\" in rule and len(value) != rule[\"length\"]:\n", " errs.append(f\"{name}: ๊ธธ์ด {rule['length']} ํ•„์š”\")\n", " if \"pattern\" in rule and not re.match(rule[\"pattern\"], value):\n", " errs.append(f\"{name}: ํ˜•์‹ ๋ถˆ์ผ์น˜ '{value}'\")\n", " return errs\n", "\n", "\n", "def validate(rec):\n", " errs = [f\"{k}: ํ•„์ˆ˜ ํ•„๋“œ ๋ˆ„๋ฝ\" for k in SCHEMA[\"required\"] if k not in rec]\n", " for k, rule in SCHEMA[\"fields\"].items():\n", " if k in rec:\n", " errs += check(k, rec[k], rule)\n", " for group in (\"sensor\", \"tactical\"):\n", " for k, rule in SCHEMA[group].items():\n", " if k not in rec.get(group, {}):\n", " errs.append(f\"{group}.{k}: ํ•„์ˆ˜ ํ•„๋“œ ๋ˆ„๋ฝ\")\n", " else:\n", " errs += check(f\"{group}.{k}\", rec[group][k], rule)\n", " for i, lab in enumerate(rec.get(\"labels\", [])):\n", " for k, rule in SCHEMA[\"label\"].items():\n", " if k not in lab:\n", " errs.append(f\"labels[{i}].{k}: ํ•„์ˆ˜ ํ•„๋“œ ๋ˆ„๋ฝ\")\n", " else:\n", " errs += check(f\"labels[{i}].{k}\", lab[k], rule)\n", " return errs\n", "\n", "\n", "# 2) [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ์ƒ˜ํ”Œ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ 20๊ฑด ์ƒ์„ฑ\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": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 01 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\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", "
\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", " (s0, s1), (h0, h1) = PROFILE[cls]\n", " x, y = xy if xy else (rng.uniform(6, W - 12), rng.uniform(6, H - 12))\n", " speed = {\"drone\": 1.3, \"bird\": 0.9, \"civil\": 0.35, \"clutter\": 0.0}[cls]\n", " vx, vy = v if v is not None else rng.uniform(-speed, speed, 2)\n", " return {\"cls\": cls, \"size\": int(rng.integers(s0, s1 + 1)), \"heat\": float(rng.uniform(h0, h1)),\n", " \"x\": float(x), \"y\": float(y), \"vx\": float(vx), \"vy\": float(vy), \"dive\": dive}\n", "\n", "\n", "def _noise(img, cond, rng):\n", " f = img.astype(float)\n", " if cond == \"night\":\n", " f = f * 0.8 + rng.normal(0, 14, f.shape)\n", " elif cond == \"fog\":\n", " f = f * 0.55 + 55 + rng.normal(0, 9, f.shape)\n", " elif cond == \"rain\":\n", " f = f + rng.normal(0, 8, f.shape)\n", " for _ in range(20):\n", " x, y0 = rng.integers(0, W), rng.integers(0, H - 8)\n", " f[y0:y0 + 8, x] += 45\n", " else:\n", " f = f + rng.normal(0, 6, f.shape)\n", " return np.clip(f, 0, 255).astype(np.uint8)\n", "\n", "\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": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 02 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\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", "
\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", " (s0, s1), (h0, h1) = PROFILE[cls]\n", " x, y = xy if xy else (rng.uniform(6, W - 12), rng.uniform(6, H - 12))\n", " speed = {\"drone\": 1.3, \"bird\": 0.9, \"civil\": 0.35, \"clutter\": 0.0}[cls]\n", " vx, vy = v if v is not None else rng.uniform(-speed, speed, 2)\n", " return {\"cls\": cls, \"size\": int(rng.integers(s0, s1 + 1)), \"heat\": float(rng.uniform(h0, h1)),\n", " \"x\": float(x), \"y\": float(y), \"vx\": float(vx), \"vy\": float(vy), \"dive\": dive}\n", "\n", "\n", "def _noise(img, cond, rng):\n", " f = img.astype(float)\n", " if cond == \"night\":\n", " f = f * 0.8 + rng.normal(0, 14, f.shape)\n", " elif cond == \"fog\":\n", " f = f * 0.55 + 55 + rng.normal(0, 9, f.shape)\n", " elif cond == \"rain\":\n", " f = f + rng.normal(0, 8, f.shape)\n", " for _ in range(20):\n", " x, y0 = rng.integers(0, W), rng.integers(0, H - 8)\n", " f[y0:y0 + 8, x] += 45\n", " else:\n", " f = f + rng.normal(0, 6, f.shape)\n", " return np.clip(f, 0, 255).astype(np.uint8)\n", "\n", "\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": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 03 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\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", "
\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", " (s0, s1), (h0, h1) = PROFILE[cls]\n", " x, y = xy if xy else (rng.uniform(6, W - 12), rng.uniform(6, H - 12))\n", " speed = {\"drone\": 1.3, \"bird\": 0.9, \"civil\": 0.35, \"clutter\": 0.0}[cls]\n", " vx, vy = v if v is not None else rng.uniform(-speed, speed, 2)\n", " return {\"cls\": cls, \"size\": int(rng.integers(s0, s1 + 1)), \"heat\": float(rng.uniform(h0, h1)),\n", " \"x\": float(x), \"y\": float(y), \"vx\": float(vx), \"vy\": float(vy), \"dive\": dive}\n", "\n", "\n", "def _noise(img, cond, rng):\n", " f = img.astype(float)\n", " if cond == \"night\":\n", " f = f * 0.8 + rng.normal(0, 14, f.shape)\n", " elif cond == \"fog\":\n", " f = f * 0.55 + 55 + rng.normal(0, 9, f.shape)\n", " elif cond == \"rain\":\n", " f = f + rng.normal(0, 8, f.shape)\n", " for _ in range(20):\n", " x, y0 = rng.integers(0, W), rng.integers(0, H - 8)\n", " f[y0:y0 + 8, x] += 45\n", " else:\n", " f = f + rng.normal(0, 6, f.shape)\n", " return np.clip(f, 0, 255).astype(np.uint8)\n", "\n", "\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": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 04 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\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", "
\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", " (s0, s1), (h0, h1) = PROFILE[cls]\n", " x, y = xy if xy else (rng.uniform(6, W - 12), rng.uniform(6, H - 12))\n", " speed = {\"drone\": 1.3, \"bird\": 0.9, \"civil\": 0.35, \"clutter\": 0.0}[cls]\n", " vx, vy = v if v is not None else rng.uniform(-speed, speed, 2)\n", " return {\"cls\": cls, \"size\": int(rng.integers(s0, s1 + 1)), \"heat\": float(rng.uniform(h0, h1)),\n", " \"x\": float(x), \"y\": float(y), \"vx\": float(vx), \"vy\": float(vy), \"dive\": dive}\n", "\n", "\n", "def _noise(img, cond, rng):\n", " f = img.astype(float)\n", " if cond == \"night\":\n", " f = f * 0.8 + rng.normal(0, 14, f.shape)\n", " elif cond == \"fog\":\n", " f = f * 0.55 + 55 + rng.normal(0, 9, f.shape)\n", " elif cond == \"rain\":\n", " f = f + rng.normal(0, 8, f.shape)\n", " for _ in range(20):\n", " x, y0 = rng.integers(0, W), rng.integers(0, H - 8)\n", " f[y0:y0 + 8, x] += 45\n", " else:\n", " f = f + rng.normal(0, 6, f.shape)\n", " return np.clip(f, 0, 255).astype(np.uint8)\n", "\n", "\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": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 05 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\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", "
\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", " rows = []\n", " for i in range(N):\n", " az = (np.degrees(np.arctan2(pos[i, 0], pos[i, 1])) + 360) % 360\n", " rng_km = float(np.linalg.norm(pos[i]))\n", " closing = -float(np.dot(pos[i], vel[i]) / max(rng_km, 1e-6)) / 1000 # km/s ์ ‘๊ทผ ์†๋„\n", " tti = rng_km / closing if closing > 0 else np.inf\n", " if vz[i] < -2: # ํ•˜๊ฐ• ์ค‘์ด๋ฉด ๊ณ ๋„ 0 ๋„๋‹ฌ ์ง€์ (์˜ˆ์ƒ ํƒ„์ฐฉ์ ) ์™ธ์‚ฝ\n", " t_hit = alt[i] / -vz[i]\n", " impact = pos[i] + vel[i] * t_hit / 1000\n", " else:\n", " impact = np.array([np.nan, np.nan])\n", " in_zone = rng_km <= FIRE_ZONE_KM\n", " rows.append({\"ID\": f\"T{i:02d}\", \"์œ ํ˜•(์ถ”์ •)\": kinds[i], \"๋ฐฉ์œ„(๋„)\": round(az), \"๊ฑฐ๋ฆฌ(km)\": round(rng_km, 1),\n", " \"๊ณ ๋„(m)\": int(alt[i]), \"์†๋„(m/s)\": int(np.linalg.norm(vel[i])), \"๋„๋‹ฌ์˜ˆ์ƒ(s)\": None if np.isinf(tti) else int(tti),\n", " \"์˜ˆ์ƒํƒ„์ฐฉ์ (km)\": None if np.isnan(impact[0]) else f\"({impact[0]:.1f}, {impact[1]:.1f})\",\n", " \"์‚ฌ๊ฒฉ๊ตฌ์—ญ ๋‚ด\": in_zone, \"์‹ ๋ขฐ๋„\": round(float(score[i]), 2),\n", " \"๊ธด๊ธ‰\": bool(score[i] >= 0.5 and (in_zone or (not np.isinf(tti) and tti < 120)))})\n", " return pd.DataFrame(rows), [None if np.isnan(r[0]) else r for r in\n", " [pos[i] + vel[i] * (alt[i] / -vz[i]) / 1000 if vz[i] < -2 else np.array([np.nan, np.nan]) for i in range(N)]]\n", "\n", "\n", "# 1) 1์ดˆ ์ฃผ๊ธฐ ๊ฐฑ์‹  ๋ชจ์˜: 5ํšŒ ๊ฐฑ์‹ ํ•˜๋ฉฐ ์ฒ˜๋ฆฌ์‹œ๊ฐ„ ์ธก์ •\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": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 06 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\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", "
\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": [ "\"\"\"์ž๋™ ํƒ์ง€ ํ‘œ์ ์„ ๋ฐฉ๊ณต์ž‘์ „๊ด€์ด ์ง์ ‘ ์Šน์ธยท๋ณด๋ฅ˜ยท๋ฐ˜๋ คํ•˜๋Š” ์ธ๊ฐ„ ๊ฐœ์ž…(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": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 07 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\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", "
\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", " (s0, s1), (h0, h1) = PROFILE[cls]\n", " x, y = xy if xy else (rng.uniform(6, W - 12), rng.uniform(6, H - 12))\n", " speed = {\"drone\": 1.3, \"bird\": 0.9, \"civil\": 0.35, \"clutter\": 0.0}[cls]\n", " vx, vy = v if v is not None else rng.uniform(-speed, speed, 2)\n", " return {\"cls\": cls, \"size\": int(rng.integers(s0, s1 + 1)), \"heat\": float(rng.uniform(h0, h1)),\n", " \"x\": float(x), \"y\": float(y), \"vx\": float(vx), \"vy\": float(vy), \"dive\": dive}\n", "\n", "\n", "def _noise(img, cond, rng):\n", " f = img.astype(float)\n", " if cond == \"night\":\n", " f = f * 0.8 + rng.normal(0, 14, f.shape)\n", " elif cond == \"fog\":\n", " f = f * 0.55 + 55 + rng.normal(0, 9, f.shape)\n", " elif cond == \"rain\":\n", " f = f + rng.normal(0, 8, f.shape)\n", " for _ in range(20):\n", " x, y0 = rng.integers(0, W), rng.integers(0, H - 8)\n", " f[y0:y0 + 8, x] += 45\n", " else:\n", " f = f + rng.normal(0, 6, f.shape)\n", " return np.clip(f, 0, 255).astype(np.uint8)\n", "\n", "\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": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 08 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\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", "
\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", " (s0, s1), (h0, h1) = PROFILE[cls]\n", " x, y = xy if xy else (rng.uniform(6, W - 12), rng.uniform(6, H - 12))\n", " speed = {\"drone\": 1.3, \"bird\": 0.9, \"civil\": 0.35, \"clutter\": 0.0}[cls]\n", " vx, vy = v if v is not None else rng.uniform(-speed, speed, 2)\n", " return {\"cls\": cls, \"size\": int(rng.integers(s0, s1 + 1)), \"heat\": float(rng.uniform(h0, h1)),\n", " \"x\": float(x), \"y\": float(y), \"vx\": float(vx), \"vy\": float(vy), \"dive\": dive}\n", "\n", "\n", "def _noise(img, cond, rng):\n", " f = img.astype(float)\n", " if cond == \"night\":\n", " f = f * 0.8 + rng.normal(0, 14, f.shape)\n", " elif cond == \"fog\":\n", " f = f * 0.55 + 55 + rng.normal(0, 9, f.shape)\n", " elif cond == \"rain\":\n", " f = f + rng.normal(0, 8, f.shape)\n", " for _ in range(20):\n", " x, y0 = rng.integers(0, W), rng.integers(0, H - 8)\n", " f[y0:y0 + 8, x] += 45\n", " else:\n", " f = f + rng.normal(0, 6, f.shape)\n", " return np.clip(f, 0, 255).astype(np.uint8)\n", "\n", "\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": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 09 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\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", "
\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", " (s0, s1), (h0, h1) = PROFILE[cls]\n", " x, y = xy if xy else (rng.uniform(6, W - 12), rng.uniform(6, H - 12))\n", " speed = {\"drone\": 1.3, \"bird\": 0.9, \"civil\": 0.35, \"clutter\": 0.0}[cls]\n", " vx, vy = v if v is not None else rng.uniform(-speed, speed, 2)\n", " return {\"cls\": cls, \"size\": int(rng.integers(s0, s1 + 1)), \"heat\": float(rng.uniform(h0, h1)),\n", " \"x\": float(x), \"y\": float(y), \"vx\": float(vx), \"vy\": float(vy), \"dive\": dive}\n", "\n", "\n", "def _noise(img, cond, rng):\n", " f = img.astype(float)\n", " if cond == \"night\":\n", " f = f * 0.8 + rng.normal(0, 14, f.shape)\n", " elif cond == \"fog\":\n", " f = f * 0.55 + 55 + rng.normal(0, 9, f.shape)\n", " elif cond == \"rain\":\n", " f = f + rng.normal(0, 8, f.shape)\n", " for _ in range(20):\n", " x, y0 = rng.integers(0, W), rng.integers(0, H - 8)\n", " f[y0:y0 + 8, x] += 45\n", " else:\n", " f = f + rng.normal(0, 6, f.shape)\n", " return np.clip(f, 0, 255).astype(np.uint8)\n", "\n", "\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": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 10 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\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", "
\n" ] } ], "metadata": { "colab": { "provenance": [] }, "kernelspec": { "display_name": "Python 3", "name": "python3" }, "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 0 }