{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# ๐ฏ [TRN03] ๋๋ก ยท๋ฏธ์ฌ์ผ ์ค๋ฃจ์ฃ ์ด๋ฏธ์ง ๋ถ๋ฅ ํ์ต์ฑ\n",
"### Google Colab ์ค์ต ํ๊ฒฝ (๊ต์กยทํ๋ จ / Level 2)\n",
"\n",
"[](https://colab.research.google.com/github/edunlab/milicontent2/blob/main/uploads/TRN03/TRN03_solution.ipynb)\n",
"\n",
"---\n",
"\n",
"## ๐ 1. ํ๋ก์ ํธ ๊ฐ์ ๋ฐ ๋ฐฐ๊ฒฝ\n",
"\n",
"| ํญ๋ชฉ | ๋ด์ฉ |\n",
"| :--- | :--- |\n",
"| **๊ณผ์ ๋ถ๋ฅ** | ๊ต์กยทํ๋ จ (Level 2) |\n",
"| **ํ์ต ์์ ์๊ฐ** | 10๊ฐ ๋ฏธ์
(5์ผ / ์ด 27.5์๊ฐ) |\n",
"| **ํ๊ฒฝ ์ฌ์** | Python 3.10+, numpy, pandas, matplotlib, scipy, scikit-learn |\n",
"\n",
"### ๐ ํ๋ก์ ํธ ๋ชฉํ\n",
"๋ฌ์์-์ฐํฌ๋ผ์ด๋ ์ ์๊ณผ ๋ฏธ๊ตญ-์ด๋ ๋ถ์ ์ฌ๋ก์์๋ ์ํ ๋๋ก , ์ํญ ๋๋ก , ์ํญยทํ๋๋ฏธ์ฌ์ผ์ด ํจ๊ป ์ฐ์ด๋ฉด์, ์ฅ๋ณ์ด ํ๋์ ๋นํ์ฒด ํ์์ ๋ณด๊ณ ์ํ ์ฌ๋ถ๋ฅผ ๋นจ๋ฆฌ ๊ตฌ๋ถํ๋ ๋ฅ๋ ฅ์ด ์ค์ํ ๊ธฐ๋ณธ ์ ํฌ๋ ฅ์ด ๋์๋ค. ๋ณธ ํ๋ก์ ํธ์์๋ ์ค์ ๊ธฐ์ฒด ์ฌ์ง ๋์ ์ฌ๊ฐํยท์ผ๊ฐํยท์ ๊ฐ์ ๊ธฐ๋ณธ ๋ํ์ผ๋ก ๋ง๋ ๊ต์ก์ฉ ํฉ์ฑ ์ค๋ฃจ์ฃ์ ์ฝ๋๋ก ์์ฑํด, ๋ ์ด๋ธ ์ฒด๊ณ ์ ์, ๋ช
์ยท์ค๊ณฝ์ ยทํฌ๊ธฐ ์ ๊ทํ ์ ์ฒ๋ฆฌ, ํํ ํน์ง ๊ธฐ๋ฐ Baseline, ๋ค์ํ ๊ฐ๋ยท์๊ฑฐ๋ฆฌยทํ๋ฆผ ์กฐ๊ฑด์ ์ ๊ฒฝ๋ง ๋ถ๋ฅ ๊ณ ๋ํ, ์ ์ฌ ํ์ ์ค๋ถ๋ฅ ๋ฐฉ์ง ์ ๋ขฐ๋ ์ ์, ํ์ต ์ฑ UI, ๊ต๊ด ํด์ค ๊ฒํ (HITL), ๋๋ ์ ํ์ง ์ด๋ฏธ์ง ๋ถํ ์ํ, AAR ์ฆ๊ฐ ์ฌํ์ต, ์ต์ข
ํจํค์ง๊น์ง ์ค๋ฃจ์ฃ ์๋ณ ํ์ต์ฑ์ ๋จ๊ณ๋ณ๋ก ๊ตฌํํ๋ค. ์ต์ข
๋ชฉํ๋ AI๊ฐ ๋ฌธํญ ์ถ์ ์ ์ฐธ๊ณ ํ์ ์ ๋๋, ๋ถํ์คํ ํ์ ๊ณผ ์ค๋ต ํด์ค์ ๋ฐ๋์ ๊ต๊ด์ด ํ์ธํ ๋ค ์ฅ๋ณ์๊ฒ ์ ๊ณต๋๋ ํ์ต ์ฒด๊ณ๋ฅผ ๋ง๋๋ ๊ฒ์ด๋ค.\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",
"์ ์ ๋๋ก ๋ฐ ์ํญ/ํ๋๋ฏธ์ฌ์ผ ์ค๋ฃจ์ฃ ์ด๋ฏธ์ง ๋ฐ์ดํฐ์
์ ๊ตฌ์ถํ๊ณ ๋ ์ด๋ธ์ ์ ์ํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ ์ ๋๋ก ๊ณผ ์ํญยทํ๋๋ฏธ์ฌ์ผ ๋ฑ ๋นํ์ฒด ์ค๋ฃจ์ฃ ์ด๋ฏธ์ง ํ์ต ๋ฐ์ดํฐ์ ๋ ์ด๋ธ ์ฒด๊ณ์ ๋ฉํ๋ฐ์ดํฐ ์คํค๋ง๋ฅผ ์ ์ํ๊ณ , ์๋ชป๋ ๋ฐ์ดํฐ๋ฅผ ๊ฑธ๋ฌ๋ด๋ ๊ฒ์ฆ๊ธฐ๋ฅผ ๋ง๋ ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๋ฏผ๊ฐ ์ฟผ๋์ฝฅํฐํยท๋ฏผ๊ฐ ๊ณ ์ ์ตํยท์ผ๊ฐ์ต ์ํญ๋๋ก ํยท์ํญ๋ฏธ์ฌ์ผํยทํ๋๋ฏธ์ฌ์ผํยท์กฐ๋ฅ 6์ข
๋ ์ด๋ธ๊ณผ ์ํ ์ฌ๋ถ, ์ดฌ์ ์กฐ๊ฑด(๊ธฐ๋ณธยท์๊ฑฐ๋ฆฌยท์ธก๋ฉดยท์ผ๊ฐยทํ๋ฆผ), ์ผ์(EO/IR), ์ ์ ํ๋ผ๋ฏธํฐ(๊ธฐ์, ๊ฑฐ๋ฆฌ ๋ฑ๊ธ, ์์น์ค์ฐจ, ํต์ ์ง์ฐ), ๊ต๊ด ๊ฒ์ ์ํ ํ๋ ์ ์ํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `data/schema/trn03_input_schema.json ๋ฐ docs/trn03_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": [
"\"\"\"์ ์ ๋๋ก ยท์ํญ/ํ๋๋ฏธ์ฌ์ผ ๋ฑ ๋นํ์ฒด ์ค๋ฃจ์ฃ ์ด๋ฏธ์ง(ํ๋ฐฑยท์๊ฑฐ๋ฆฌยท๋ค๊ฐ๋) ํ์ต ๋ฐ์ดํฐ์
์ ๋ ์ด๋ธ ์ฒด๊ณ์ ๋ฉํ๋ฐ์ดํฐ ์คํค๋ง๋ฅผ ์ ์ํ๊ณ , ํฉ์ฑ ์ค๋ฃจ์ฃ ์ํ์ ๋ง๋ค์ด ๊ฒ์ฆํ๋ค.\"\"\"\n",
"import json\n",
"import re\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from matplotlib.path import Path as MplPath\n",
"from scipy import ndimage\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ํฉ์ฑ ์ค๋ฃจ์ฃ ์์ฑ๊ธฐ (์ค์ ๊ธฐ์ฒด ์ฌ์งยท๋๋ฉดยท์ ์ ์ฌ์ฉ ์ ํจ)\n",
"# ์ฌ๊ฐํยท์ผ๊ฐํยท์ ๊ฐ์ ๊ธฐ๋ณธ ๋ํ์ ์กฐํฉํ ์ถ์ ์ค๋ฃจ์ฃ์ 32x32 ํ๋ฐฑ ์ด๋ฏธ์ง๋ก ๊ทธ๋ฆฐ๋ค.\n",
"# ํ์ (๋ค๊ฐ๋), ์๊ทผ ์ถ์(์ธก๋ฉด ์์ ), ํฌ๊ธฐ(์๊ฑฐ๋ฆฌ), ํ๋ฆผยท์ก์ยท์ ๋๋นยท๊ตฌ๋ฆยท๊ฐ๋ฆผยท์ผ๊ฐ ๋ฐ์ ์ ์กฐ์ ํ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"IMG = 32\n",
"CLASSES = [\"civil_quad\", \"civil_fw\", \"attack_fw\", \"cruise\", \"ballistic\", \"bird\"]\n",
"KOR = {\"civil_quad\": \"๋ฏผ๊ฐ ์ฟผ๋์ฝฅํฐํ\", \"civil_fw\": \"๋ฏผ๊ฐ ๊ณ ์ ์ตํ\", \"attack_fw\": \"์ผ๊ฐ์ต ์ํญ๋๋ก ํ\",\n",
" \"cruise\": \"์ํญ๋ฏธ์ฌ์ผํ\", \"ballistic\": \"ํ๋๋ฏธ์ฌ์ผํ\", \"bird\": \"์กฐ๋ฅ\"}\n",
"THREAT = {\"attack_fw\", \"cruise\", \"ballistic\"}\n",
"\n",
"\n",
"def _rect(x0, x1, y0, y1):\n",
" return [(x0, y0), (x1, y0), (x1, y1), (x0, y1)]\n",
"\n",
"\n",
"def _bar(p, q, w):\n",
" (x0, y0), (x1, y1) = p, q\n",
" n = np.array([y0 - y1, x1 - x0]) / np.hypot(x1 - x0, y1 - y0) * w / 2\n",
" return [(x0 + n[0], y0 + n[1]), (x1 + n[0], y1 + n[1]), (x1 - n[0], y1 - n[1]), (x0 - n[0], y0 - n[1])]\n",
"\n",
"\n",
"# ๋ํ ์ ์(๋จ์ ์ขํ, ๊ธฐ์ ๋ฐฉํฅ = +y). ๋ชจ๋ ์ถ์ ๋ํ์ด๋ฉฐ ํน์ ๊ธฐ์ข
์ ๋ณธ๋จ์ง ์๋๋ค.\n",
"SHAPES = {\n",
" \"civil_quad\": {\"poly\": [_bar((-.55, -.55), (.55, .55), .1), _bar((-.55, .55), (.55, -.55), .1), _rect(-.15, .15, -.15, .15)],\n",
" \"circ\": [(-.55, -.55, .24), (.55, .55, .24), (-.55, .55, .24), (.55, -.55, .24)]},\n",
" \"civil_fw\": {\"poly\": [_rect(-.07, .07, -.8, .72), _rect(-.9, .9, .05, .25), _rect(-.33, .33, -.8, -.66)], \"circ\": []},\n",
" \"attack_fw\": {\"poly\": [[(0, .85), (.85, -.5), (.12, -.42), (0, -.6), (-.12, -.42), (-.85, -.5)]], \"circ\": []},\n",
" \"cruise\": {\"poly\": [_rect(-.07, .07, -.9, .7), [(-.07, .7), (0, .92), (.07, .7)], _rect(-.42, .42, -.05, .07),\n",
" [(-.07, -.9), (-.24, -.95), (-.07, -.74)], [(.07, -.9), (.24, -.95), (.07, -.74)]], \"circ\": []},\n",
" \"ballistic\": {\"poly\": [_rect(-.13, .13, -.85, .6), [(-.13, .6), (0, .95), (.13, .6)],\n",
" [(-.13, -.85), (-.34, -.95), (-.13, -.58)], [(.13, -.85), (.34, -.95), (.13, -.58)]], \"circ\": []},\n",
" \"bird\": {\"poly\": [[(-.9, .12), (-.5, .28), (-.12, .06), (0, .22), (.12, .06), (.5, .28), (.9, .12), (.5, .12),\n",
" (.12, -.1), (0, -.38), (-.12, -.1), (-.5, .12)]], \"circ\": []},\n",
"}\n",
"_S = 2\n",
"_g = (np.arange(IMG * _S) + 0.5) / _S\n",
"_GX, _GY = np.meshgrid(_g, _g)\n",
"_PTS = np.c_[_GX.ravel(), _GY.ravel()]\n",
"_PATHS = {c: [MplPath(np.array(p)) for p in s[\"poly\"]] for c, s in SHAPES.items()}\n",
"\n",
"\n",
"def draw(cls, rng, angle=None, scale=None, fore=None, blur=0.3, noise=4.0, contrast=1.0,\n",
" clutter=0.0, occlude=0.0, invert=False, shift=1.5):\n",
" \"\"\"ํด๋์ค 1๊ฐ ์ค๋ฃจ์ฃ ์ด๋ฏธ์ง(uint8 32x32)๋ฅผ ๊ทธ๋ฆฐ๋ค. ๋ฐฐ๊ฒฝ์ ๋ฐ์ ํ๋, ๋นํ์ฒด๋ ์ด๋์ด ๊ทธ๋ฆผ์.\"\"\"\n",
" angle = rng.uniform(0, 360) if angle is None else angle\n",
" scale = rng.uniform(.7, .95) if scale is None else scale\n",
" fore = rng.uniform(.85, 1.0) if fore is None else fore\n",
" th = np.radians(angle)\n",
" c0 = IMG / 2 + rng.uniform(-shift, shift, 2)\n",
" d = (_PTS - c0) / (scale * IMG / 2) # ์ด๋ฏธ์ง ์ขํ -> ๋ํ ์ขํ(์ญ๋ณํ)\n",
" u = d[:, 0] * np.cos(th) + d[:, 1] * np.sin(th)\n",
" v = (-d[:, 0] * np.sin(th) + d[:, 1] * np.cos(th)) / fore # ์๊ทผ ์ถ์(๊ธฐ์ ๋ฐฉํฅ์ผ๋ก ์งง์ ๋ณด์)\n",
" uv = np.c_[u, -v]\n",
" m = np.zeros(len(uv), bool)\n",
" near = np.flatnonzero(np.abs(uv).max(axis=1) <= 1.0) # ๋ํ์ด ์์ ์ ์๋ ์์ญ๋ง ๊ฒ์ฌ(์๋)\n",
" for p in _PATHS[cls]:\n",
" m[near] |= p.contains_points(uv[near])\n",
" for cx, cy, r in SHAPES[cls][\"circ\"]:\n",
" m |= (uv[:, 0] - cx) ** 2 + (uv[:, 1] - cy) ** 2 <= r * r\n",
" cov = m.reshape(IMG, _S, IMG, _S).mean(axis=(1, 3))\n",
" if occlude > 0: # ๋ถ๋ถ ๊ฐ๋ฆผ(๊ตฌ๋ฆ ์กฐ๊ฐ)\n",
" h = int(IMG * np.sqrt(occlude)); y0, x0 = rng.integers(8, IMG - 8 - h // 2, 2)\n",
" cov[y0:y0 + h, x0:x0 + h] = 0\n",
" bg = rng.uniform(180, 220) + np.linspace(-10, 10, IMG)[:, None]\n",
" fg = rng.uniform(30, 60)\n",
" img = bg * (1 - cov) + fg * cov\n",
" if clutter > 0: # ๊ตฌ๋ฆยท๋ฐฐ๊ฒฝ ์ผ๋ฃฉ\n",
" img -= clutter * 90 * np.clip(ndimage.gaussian_filter(rng.normal(0, 1, (IMG, IMG)), 3) * 4, 0, None)\n",
" if blur > 0:\n",
" img = ndimage.gaussian_filter(img, blur)\n",
" mean = img.mean()\n",
" img = mean + (img - mean) * contrast\n",
" img = img + rng.normal(0, noise, img.shape)\n",
" if invert: # ์ผ๊ฐ ์ด์(IR) ๊ทน์ฑ ๋ฐ์ \n",
" img = 255 - img\n",
" return np.clip(img, 0, 255).astype(np.uint8)\n",
"\n",
"\n",
"# ์ดฌ์ ์กฐ๊ฑด ํ๋ฆฌ์
: ๊ธฐ๋ณธ + ๋ณตํฉ ์ํ ์๋๋ฆฌ์ค\n",
"CONDITIONS = {\n",
" \"basic\": {},\n",
" \"offcenter\": {\"shift\": 5.0, \"fore\": (.6, .85)},\n",
" \"far\": {\"scale\": (.32, .45), \"blur\": .6},\n",
" \"blur\": {\"blur\": (1.2, 1.8)},\n",
" \"lowcon\": {\"contrast\": (.2, .35)},\n",
" \"clutter\": {\"clutter\": .8},\n",
" \"occlude\": {\"occlude\": .08},\n",
" \"oblique\": {\"fore\": (.35, .6)},\n",
" \"night\": {\"invert\": True, \"noise\": 10.0},\n",
" \"noise\": {\"noise\": 28.0},\n",
" \"combo\": {\"scale\": (.4, .55), \"blur\": (.8, 1.1), \"noise\": 12.0, \"contrast\": (.6, .8)},\n",
"}\n",
"\n",
"\n",
"def draw_cond(cls, cond, rng):\n",
" kw = {}\n",
" for k, v in CONDITIONS[cond].items():\n",
" kw[k] = rng.uniform(*v) if isinstance(v, tuple) else v\n",
" return draw(cls, rng, **kw)\n",
"\n",
"\n",
"def make_dataset(n_per_class, seed, conds=(\"basic\",)):\n",
" \"\"\"(์ด๋ฏธ์ง ๋ฐฐ์ด N x 32 x 32, ์ ๋ต ๋ผ๋ฒจ, ์กฐ๊ฑด) ๋ฐํ. ์กฐ๊ฑด์ conds์์ ๋ฒ๊ฐ์ ๊ณ ๋ฅธ๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" X, y, c = [], [], []\n",
" for cls in CLASSES:\n",
" for i in range(n_per_class):\n",
" cond = conds[i % len(conds)]\n",
" X.append(draw_cond(cls, cond, rng)); y.append(cls); c.append(cond)\n",
" return np.array(X), np.array(y), np.array(c)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# 1) ์คํค๋ง ์ ์: ์ด๋ฏธ์ง ๊ท๊ฒฉ, ๋ ์ด๋ธ(ํด๋์คยท์ํ ์ฌ๋ถ), ์ดฌ์ ์กฐ๊ฑด, ์ ์ ํ๋ผ๋ฏธํฐ, ๊ฒ์ ์ํ\n",
"SCHEMA = {\n",
" \"fields\": {\n",
" \"image_id\": {\"type\": str, \"pattern\": r\"^SIL\\d{5}$\"},\n",
" \"class\": {\"type\": str, \"enum\": CLASSES},\n",
" \"threat\": {\"type\": bool},\n",
" \"condition\": {\"type\": str, \"enum\": list(CONDITIONS)},\n",
" \"view\": {\"type\": str, \"enum\": [\"top\", \"oblique\"]},\n",
" \"source\": {\"type\": str, \"enum\": [\"synthetic\"]},\n",
" \"reviewed_by\": {\"type\": str, \"pattern\": r\"^INS\\d{2}$\"},\n",
" \"review_status\": {\"type\": str, \"enum\": [\"draft\", \"approved\"]},\n",
" },\n",
" \"sensor\": {\"type\": {\"type\": str, \"enum\": [\"EO\", \"IR\"]}, \"resolution\": {\"type\": list, \"length\": 2}},\n",
" \"tactical\": {\n",
" \"weather\": {\"type\": str, \"enum\": [\"clear\", \"haze\", \"cloud\", \"night\", \"rain\"]},\n",
" \"range_level\": {\"type\": str, \"enum\": [\"near\", \"mid\", \"far\"]},\n",
" \"position_error_m\": {\"type\": (int, float), \"min\": 0, \"max\": 500},\n",
" \"comm_delay_ms\": {\"type\": (int, float), \"min\": 0, \"max\": 10000},\n",
" },\n",
"}\n",
"\n",
"\n",
"def check(name, value, rule):\n",
" if not isinstance(value, rule[\"type\"]) or (isinstance(value, bool) and rule[\"type\"] is not 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 = []\n",
" for k, rule in SCHEMA[\"fields\"].items():\n",
" errs += [f\"{k}: ํ์ ํ๋ ๋๋ฝ\"] if k not in rec else check(k, rec[k], rule)\n",
" for g in (\"sensor\", \"tactical\"):\n",
" for k, rule in SCHEMA[g].items():\n",
" errs += [f\"{g}.{k}: ํ์ ํ๋ ๋๋ฝ\"] if k not in rec.get(g, {}) else check(f\"{g}.{k}\", rec[g][k], rule)\n",
" px = np.asarray(rec.get(\"pixels\", []), dtype=float)\n",
" if px.shape != (IMG, IMG):\n",
" errs.append(f\"pixels: ํฌ๊ธฐ {IMG}x{IMG} ํ์(์
๋ ฅ {px.shape})\")\n",
" elif not np.isfinite(px).all() or px.min() < 0 or px.max() > 255:\n",
" errs.append(\"pixels: ๊ฐ ๋ฒ์(0~255) ๋๋ ๊ฒฐ์ธก ์ค๋ฅ\")\n",
" if rec.get(\"class\") in CLASSES and rec.get(\"threat\") != (rec[\"class\"] in THREAT):\n",
" errs.append(\"threat: ํด๋์ค์ ์ํ ์ฌ๋ถ ๋ถ์ผ์น\")\n",
" return errs\n",
"\n",
"\n",
"# 2) [๊ต์ก์ฉ๊ฐ์์๋ฃ] ํด๋์ค๋ณ 10์ฅ, ์ด 60์ฅ ์ํ ์์ฑ(๊ธฐ๋ณธ + ์๊ฑฐ๋ฆฌ + ์ธก๋ฉด + ์ผ๊ฐ ์กฐ๊ฑด)\n",
"rng = np.random.default_rng(1)\n",
"conds = [\"basic\", \"far\", \"oblique\", \"night\", \"blur\"]\n",
"weather = {\"basic\": \"clear\", \"far\": \"haze\", \"oblique\": \"clear\", \"night\": \"night\", \"blur\": \"rain\"}\n",
"records = []\n",
"for cls in CLASSES:\n",
" for i in range(10):\n",
" cond = conds[i % len(conds)]\n",
" records.append({\"image_id\": f\"SIL{len(records):05d}\", \"class\": cls, \"threat\": cls in THREAT,\n",
" \"condition\": cond, \"view\": \"oblique\" if cond == \"oblique\" else \"top\", \"source\": \"synthetic\",\n",
" \"reviewed_by\": \"INS01\", \"review_status\": \"approved\",\n",
" \"sensor\": {\"type\": \"IR\" if cond == \"night\" else \"EO\", \"resolution\": [IMG, IMG]},\n",
" \"tactical\": {\"weather\": weather[cond], \"range_level\": \"far\" if cond == \"far\" else \"mid\",\n",
" \"position_error_m\": float(round(rng.uniform(5, 60), 1)), \"comm_delay_ms\": int(rng.integers(20, 300))},\n",
" \"pixels\": draw_cond(cls, cond, rng).tolist()})\n",
"\n",
"ok = sum(1 for r in records if not validate(r))\n",
"print(f\"์ ์ ์ํ {len(records)}๊ฑด ์ค ๊ฒ์ฆ ํต๊ณผ {ok}๊ฑด, ์ค๋ฅ {len(records) - ok}๊ฑด (๋ชฉํ ์ค๋ฅ 0๊ฑด: {'๋ฌ์ฑ' if ok == len(records) else '๋ฏธ๋ฌ'})\")\n",
"\n",
"# 3) ์ค๋ฅ ์ฃผ์
์ํ\n",
"bad = json.loads(json.dumps(records[0]))\n",
"bad[\"image_id\"] = \"IMG-7\"\n",
"bad[\"class\"] = \"ufo\"\n",
"bad[\"threat\"] = \"yes\"\n",
"del bad[\"tactical\"][\"comm_delay_ms\"]\n",
"bad[\"pixels\"] = bad[\"pixels\"][:20]\n",
"bad2 = json.loads(json.dumps(records[25]))\n",
"bad2[\"threat\"] = False # ์ํญ๋๋ก ํ์ธ๋ฐ ๋น์ํ์ผ๋ก ์๋ชป ํ๊ธฐ\n",
"bad2[\"pixels\"][3][3] = 300\n",
"print(\"\\n[์ค๋ฅ ์ฃผ์
๊ฒฐ๊ณผ]\")\n",
"for e in validate(bad) + validate(bad2):\n",
" print(\" -\", e)\n",
"\n",
"# 4) ๋ ์ด๋ธ ์ ์์์ ํ๋ ๋ช
์ธ\n",
"print(\"\\n[๋ ์ด๋ธ ์ ์(์ถ์ ๋ํ ๊ธฐ์ค)]\")\n",
"desc = {\"civil_quad\": \"์ญ์ ํ + ์ํ ํ์ ๋ ๊ฐ 4๊ฐ\", \"civil_fw\": \"์ง์ ์ฃผ์ต + ๊ผฌ๋ฆฌ๋ ๊ฐ\", \"attack_fw\": \"์ผ๊ฐํ(๋ธํ) ๋ ๊ฐ, ๊ผฌ๋ฆฌ ์์\",\n",
" \"cruise\": \"๊ฐ๋๊ณ ๊ธด ๋์ฒด + ์์ ์ค๊ฐ ๋ ๊ฐ\", \"ballistic\": \"๊ตต์ ์ํต ๋์ฒด + ๋พฐ์กฑํ ๋จธ๋ฆฌ + ๊ผฌ๋ฆฌ ํ\", \"bird\": \"๊ฐ๋งค๊ธฐํ ๊ตฝ์ ๋ ๊ฐ\"}\n",
"print(pd.DataFrame({\"๋ ์ด๋ธ\": CLASSES, \"ํ๊ธ๋ช
\": [KOR[c] for c in CLASSES], \"์ํ\": [\"์\" if c in THREAT else \"์๋์ค\" for c in CLASSES],\n",
" \"์๋ณ ํต์ฌ ํน์ง\": [desc[c] for c in CLASSES]}).to_string(index=False))\n",
"rows = [(g, k, (r[\"type\"].__name__ if isinstance(r[\"type\"], type) else \"number\"),\n",
" \", \".join(f\"{x}={y}\" for x, y in r.items() if x != \"type\")[:60]) for g in (\"fields\", \"sensor\", \"tactical\") for k, r in SCHEMA[g].items()]\n",
"print(\"\\n[ํ๋ ๋ช
์ธ ์์ฝ] ํ๋\", len(rows) + 1, \"๊ฐ(pixels ํฌํจ), ์ ์ ์์ฑ๋ 100%\")\n",
"print(pd.DataFrame(rows, columns=[\"๊ทธ๋ฃน\", \"ํ๋\", \"์๋ฃํ\", \"๊ท์น\"]).to_string(index=False))\n",
"\n",
"# 5) ์ํ ์ค๋ฃจ์ฃ ํ์ธ\n",
"fig, ax = plt.subplots(len(CLASSES), len(conds), figsize=(8, 9))\n",
"for i, cls in enumerate(CLASSES):\n",
" for j, cond in enumerate(conds):\n",
" r = next(x for x in records if x[\"class\"] == cls and x[\"condition\"] == cond)\n",
" ax[i, j].imshow(r[\"pixels\"], cmap=\"gray\", vmin=0, vmax=255); ax[i, j].axis(\"off\")\n",
" ax[i, j].set_title(f\"{cls}/{cond}\", fontsize=7)\n",
"plt.suptitle(\"Synthetic abstract silhouettes (training data only)\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
01 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"์ ์ ์ํ 60๊ฑด ์ค ๊ฒ์ฆ ํต๊ณผ 60๊ฑด, ์ค๋ฅ 0๊ฑด (๋ชฉํ ์ค๋ฅ 0๊ฑด: ๋ฌ์ฑ)\n",
"\n",
"[์ค๋ฅ ์ฃผ์
๊ฒฐ๊ณผ]\n",
" - image_id: ํ์ ๋ถ์ผ์น 'IMG-7'\n",
" - class: ํ์ฉ๋์ง ์์ ๊ฐ 'ufo'\n",
" - threat: ์๋ฃํ ์ค๋ฅ(str)\n",
" - tactical.comm_delay_ms: ํ์ ํ๋ ๋๋ฝ\n",
" - pixels: ํฌ๊ธฐ 32x32 ํ์(์
๋ ฅ (20, 32))\n",
" - pixels: ๊ฐ ๋ฒ์(0~255) ๋๋ ๊ฒฐ์ธก ์ค๋ฅ\n",
" - threat: ํด๋์ค์ ์ํ ์ฌ๋ถ ๋ถ์ผ์น\n",
"\n",
"[๋ ์ด๋ธ ์ ์(์ถ์ ๋ํ ๊ธฐ์ค)]\n",
" ๋ ์ด๋ธ ํ๊ธ๋ช
์ํ ์๋ณ ํต์ฌ ํน์ง\n",
"civil_quad ๋ฏผ๊ฐ ์ฟผ๋์ฝฅํฐํ ์๋์ค ์ญ์ ํ + ์ํ ํ์ ๋ ๊ฐ 4๊ฐ\n",
" civil_fw ๋ฏผ๊ฐ ๊ณ ์ ์ตํ ์๋์ค ์ง์ ์ฃผ์ต + ๊ผฌ๋ฆฌ๋ ๊ฐ\n",
" attack_fw ์ผ๊ฐ์ต ์ํญ๋๋ก ํ ์ ์ผ๊ฐํ(๋ธํ) ๋ ๊ฐ, ๊ผฌ๋ฆฌ ์์\n",
" cruise ์ํญ๋ฏธ์ฌ์ผํ ์ ๊ฐ๋๊ณ ๊ธด ๋์ฒด + ์์ ์ค๊ฐ ๋ ๊ฐ\n",
" ballistic ํ๋๋ฏธ์ฌ์ผํ ์ ๊ตต์ ์ํต ๋์ฒด + ๋พฐ์กฑํ ๋จธ๋ฆฌ + ๊ผฌ๋ฆฌ ํ\n",
" bird ์กฐ๋ฅ ์๋์ค ๊ฐ๋งค๊ธฐํ ๊ตฝ์ ๋ ๊ฐ\n",
"\n",
"[ํ๋ ๋ช
์ธ ์์ฝ] ํ๋ 15 ๊ฐ(pixels ํฌํจ), ์ ์ ์์ฑ๋ 100%\n",
" ๊ทธ๋ฃน ํ๋ ์๋ฃํ ๊ท์น\n",
" fields image_id str pattern=^SIL\\d{5}$\n",
" fields class str enum=['civil_quad', 'civil_fw', 'attack_fw', 'cruise', 'ball\n",
" fields threat bool \n",
" fields condition str enum=['basic', 'offcenter', 'far', 'blur', 'lowcon', 'clutte\n",
" fields view str enum=['top', 'oblique']\n",
" fields source str enum=['synthetic']\n",
" fields reviewed_by str pattern=^INS\\d{2}$\n",
" fields review_status str enum=['draft', 'approved']\n",
" sensor type str enum=['EO', 'IR']\n",
" sensor resolution list length=2\n",
"tactical weather str enum=['clear', 'haze', 'cloud', 'night', 'rain']\n",
"tactical range_level str enum=['near', 'mid', 'far']\n",
"tactical position_error_m number min=0, max=500\n",
"tactical comm_delay_ms number min=0, max=10000\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
02. ์ด๋ฏธ์ง ๋ช
์ ๋๋น ๊ฐํ, ์ค๊ณฝ์ (Edge) ๊ฒ์ถ ๋ฐ ํฌ๊ธฐ ์ ๊ทํ ์ ์ฒ๋ฆฌ ๋ชจ๋ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๋ช
์ ๋๋น ๊ฐํ, ์ค๊ณฝ์ (Edge) ๊ฒ์ถ ๋ฐ ํฌ๊ธฐ ์ ๊ทํ ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **ํ๋ฐฑยท์๊ฑฐ๋ฆฌยท์ผ๊ฐ ๋ฑ ์กฐ๊ฑด์ด ๋ค๋ฅธ ์ค๋ฃจ์ฃ ์ด๋ฏธ์ง๋ฅผ ๊ฐ์ ๊ธฐ์ค์ผ๋ก ๋น๊ตํ ์ ์๋๋ก ๋ช
์ ๋๋น ๊ฐํ, ์ค๊ณฝ์ ๊ฒ์ถ, ํฌ๊ธฐ ์ ๊ทํ ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๊ฒฐ์ธก ํฝ์
๋ณด์ , ์ผ๊ฐ ์ด์ ๊ทน์ฑ ์๋ ๋ฐ์ , ์ก์ ์ถ์ ๊ฐ์ ๋ฐ๋ฅธ ์ค๊ฐ๊ฐ ํํฐ ์ ์ฉ(์กฐ๊ฑด๋ณ ์๊ณ๊ฐ), ๋ช
์ ๋๋น ๊ฐํ, Sobel ์ค๊ณฝ์ ๊ฒ์ถ, ์ฃผ์ถ ๋ฐฉํฅยทํฌ๊ธฐ ์ ๊ทํ(24x24)๋ฅผ ๋ฐฐ์น ๋จ์๋ก ๊ตฌํํ๊ณ ๋์์ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/preprocess/trn03_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": [
"\"\"\"์ค๋ฃจ์ฃ ์ด๋ฏธ์ง์ ๊ฒฐ์ธก ๋ณด์ , ์ผ๊ฐ ๊ทน์ฑ ๋ณด์ , ์ก์ ์ ๊ฑฐ, ๋ช
์ ๋๋น ๊ฐํ, ์ค๊ณฝ์ (Edge) ๊ฒ์ถ, ๋ฐฉํฅยทํฌ๊ธฐ ์ ๊ทํ๋ฅผ ์ผ๊ด ์ฒ๋ฆฌํ๋ ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ๊ณ , ์ฒ๋ฆฌ ์๋ยท๋จ์ ํ
์คํธ 5์ข
ยท์ ์ฒ๋ฆฌ ์ ํ ์๋ณ ์ ํ๋๋ฅผ ํ์ธํ๋ค.\"\"\"\n",
"import time\n",
"import warnings\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from matplotlib.path import Path as MplPath\n",
"from scipy import ndimage\n",
"from scipy.spatial import ConvexHull\n",
"from sklearn.neighbors import NearestCentroid\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ํฉ์ฑ ์ค๋ฃจ์ฃ ์์ฑ๊ธฐ (์ค์ ๊ธฐ์ฒด ์ฌ์งยท๋๋ฉดยท์ ์ ์ฌ์ฉ ์ ํจ)\n",
"# ์ฌ๊ฐํยท์ผ๊ฐํยท์ ๊ฐ์ ๊ธฐ๋ณธ ๋ํ์ ์กฐํฉํ ์ถ์ ์ค๋ฃจ์ฃ์ 32x32 ํ๋ฐฑ ์ด๋ฏธ์ง๋ก ๊ทธ๋ฆฐ๋ค.\n",
"# ํ์ (๋ค๊ฐ๋), ์๊ทผ ์ถ์(์ธก๋ฉด ์์ ), ํฌ๊ธฐ(์๊ฑฐ๋ฆฌ), ํ๋ฆผยท์ก์ยท์ ๋๋นยท๊ตฌ๋ฆยท๊ฐ๋ฆผยท์ผ๊ฐ ๋ฐ์ ์ ์กฐ์ ํ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"IMG = 32\n",
"CLASSES = [\"civil_quad\", \"civil_fw\", \"attack_fw\", \"cruise\", \"ballistic\", \"bird\"]\n",
"KOR = {\"civil_quad\": \"๋ฏผ๊ฐ ์ฟผ๋์ฝฅํฐํ\", \"civil_fw\": \"๋ฏผ๊ฐ ๊ณ ์ ์ตํ\", \"attack_fw\": \"์ผ๊ฐ์ต ์ํญ๋๋ก ํ\",\n",
" \"cruise\": \"์ํญ๋ฏธ์ฌ์ผํ\", \"ballistic\": \"ํ๋๋ฏธ์ฌ์ผํ\", \"bird\": \"์กฐ๋ฅ\"}\n",
"THREAT = {\"attack_fw\", \"cruise\", \"ballistic\"}\n",
"\n",
"\n",
"def _rect(x0, x1, y0, y1):\n",
" return [(x0, y0), (x1, y0), (x1, y1), (x0, y1)]\n",
"\n",
"\n",
"def _bar(p, q, w):\n",
" (x0, y0), (x1, y1) = p, q\n",
" n = np.array([y0 - y1, x1 - x0]) / np.hypot(x1 - x0, y1 - y0) * w / 2\n",
" return [(x0 + n[0], y0 + n[1]), (x1 + n[0], y1 + n[1]), (x1 - n[0], y1 - n[1]), (x0 - n[0], y0 - n[1])]\n",
"\n",
"\n",
"# ๋ํ ์ ์(๋จ์ ์ขํ, ๊ธฐ์ ๋ฐฉํฅ = +y). ๋ชจ๋ ์ถ์ ๋ํ์ด๋ฉฐ ํน์ ๊ธฐ์ข
์ ๋ณธ๋จ์ง ์๋๋ค.\n",
"SHAPES = {\n",
" \"civil_quad\": {\"poly\": [_bar((-.55, -.55), (.55, .55), .1), _bar((-.55, .55), (.55, -.55), .1), _rect(-.15, .15, -.15, .15)],\n",
" \"circ\": [(-.55, -.55, .24), (.55, .55, .24), (-.55, .55, .24), (.55, -.55, .24)]},\n",
" \"civil_fw\": {\"poly\": [_rect(-.07, .07, -.8, .72), _rect(-.9, .9, .05, .25), _rect(-.33, .33, -.8, -.66)], \"circ\": []},\n",
" \"attack_fw\": {\"poly\": [[(0, .85), (.85, -.5), (.12, -.42), (0, -.6), (-.12, -.42), (-.85, -.5)]], \"circ\": []},\n",
" \"cruise\": {\"poly\": [_rect(-.07, .07, -.9, .7), [(-.07, .7), (0, .92), (.07, .7)], _rect(-.42, .42, -.05, .07),\n",
" [(-.07, -.9), (-.24, -.95), (-.07, -.74)], [(.07, -.9), (.24, -.95), (.07, -.74)]], \"circ\": []},\n",
" \"ballistic\": {\"poly\": [_rect(-.13, .13, -.85, .6), [(-.13, .6), (0, .95), (.13, .6)],\n",
" [(-.13, -.85), (-.34, -.95), (-.13, -.58)], [(.13, -.85), (.34, -.95), (.13, -.58)]], \"circ\": []},\n",
" \"bird\": {\"poly\": [[(-.9, .12), (-.5, .28), (-.12, .06), (0, .22), (.12, .06), (.5, .28), (.9, .12), (.5, .12),\n",
" (.12, -.1), (0, -.38), (-.12, -.1), (-.5, .12)]], \"circ\": []},\n",
"}\n",
"_S = 2\n",
"_g = (np.arange(IMG * _S) + 0.5) / _S\n",
"_GX, _GY = np.meshgrid(_g, _g)\n",
"_PTS = np.c_[_GX.ravel(), _GY.ravel()]\n",
"_PATHS = {c: [MplPath(np.array(p)) for p in s[\"poly\"]] for c, s in SHAPES.items()}\n",
"\n",
"\n",
"def draw(cls, rng, angle=None, scale=None, fore=None, blur=0.3, noise=4.0, contrast=1.0,\n",
" clutter=0.0, occlude=0.0, invert=False, shift=1.5):\n",
" \"\"\"ํด๋์ค 1๊ฐ ์ค๋ฃจ์ฃ ์ด๋ฏธ์ง(uint8 32x32)๋ฅผ ๊ทธ๋ฆฐ๋ค. ๋ฐฐ๊ฒฝ์ ๋ฐ์ ํ๋, ๋นํ์ฒด๋ ์ด๋์ด ๊ทธ๋ฆผ์.\"\"\"\n",
" angle = rng.uniform(0, 360) if angle is None else angle\n",
" scale = rng.uniform(.7, .95) if scale is None else scale\n",
" fore = rng.uniform(.85, 1.0) if fore is None else fore\n",
" th = np.radians(angle)\n",
" c0 = IMG / 2 + rng.uniform(-shift, shift, 2)\n",
" d = (_PTS - c0) / (scale * IMG / 2) # ์ด๋ฏธ์ง ์ขํ -> ๋ํ ์ขํ(์ญ๋ณํ)\n",
" u = d[:, 0] * np.cos(th) + d[:, 1] * np.sin(th)\n",
" v = (-d[:, 0] * np.sin(th) + d[:, 1] * np.cos(th)) / fore # ์๊ทผ ์ถ์(๊ธฐ์ ๋ฐฉํฅ์ผ๋ก ์งง์ ๋ณด์)\n",
" uv = np.c_[u, -v]\n",
" m = np.zeros(len(uv), bool)\n",
" near = np.flatnonzero(np.abs(uv).max(axis=1) <= 1.0) # ๋ํ์ด ์์ ์ ์๋ ์์ญ๋ง ๊ฒ์ฌ(์๋)\n",
" for p in _PATHS[cls]:\n",
" m[near] |= p.contains_points(uv[near])\n",
" for cx, cy, r in SHAPES[cls][\"circ\"]:\n",
" m |= (uv[:, 0] - cx) ** 2 + (uv[:, 1] - cy) ** 2 <= r * r\n",
" cov = m.reshape(IMG, _S, IMG, _S).mean(axis=(1, 3))\n",
" if occlude > 0: # ๋ถ๋ถ ๊ฐ๋ฆผ(๊ตฌ๋ฆ ์กฐ๊ฐ)\n",
" h = int(IMG * np.sqrt(occlude)); y0, x0 = rng.integers(8, IMG - 8 - h // 2, 2)\n",
" cov[y0:y0 + h, x0:x0 + h] = 0\n",
" bg = rng.uniform(180, 220) + np.linspace(-10, 10, IMG)[:, None]\n",
" fg = rng.uniform(30, 60)\n",
" img = bg * (1 - cov) + fg * cov\n",
" if clutter > 0: # ๊ตฌ๋ฆยท๋ฐฐ๊ฒฝ ์ผ๋ฃฉ\n",
" img -= clutter * 90 * np.clip(ndimage.gaussian_filter(rng.normal(0, 1, (IMG, IMG)), 3) * 4, 0, None)\n",
" if blur > 0:\n",
" img = ndimage.gaussian_filter(img, blur)\n",
" mean = img.mean()\n",
" img = mean + (img - mean) * contrast\n",
" img = img + rng.normal(0, noise, img.shape)\n",
" if invert: # ์ผ๊ฐ ์ด์(IR) ๊ทน์ฑ ๋ฐ์ \n",
" img = 255 - img\n",
" return np.clip(img, 0, 255).astype(np.uint8)\n",
"\n",
"\n",
"# ์ดฌ์ ์กฐ๊ฑด ํ๋ฆฌ์
: ๊ธฐ๋ณธ + ๋ณตํฉ ์ํ ์๋๋ฆฌ์ค\n",
"CONDITIONS = {\n",
" \"basic\": {},\n",
" \"offcenter\": {\"shift\": 5.0, \"fore\": (.6, .85)},\n",
" \"far\": {\"scale\": (.32, .45), \"blur\": .6},\n",
" \"blur\": {\"blur\": (1.2, 1.8)},\n",
" \"lowcon\": {\"contrast\": (.2, .35)},\n",
" \"clutter\": {\"clutter\": .8},\n",
" \"occlude\": {\"occlude\": .08},\n",
" \"oblique\": {\"fore\": (.35, .6)},\n",
" \"night\": {\"invert\": True, \"noise\": 10.0},\n",
" \"noise\": {\"noise\": 28.0},\n",
" \"combo\": {\"scale\": (.4, .55), \"blur\": (.8, 1.1), \"noise\": 12.0, \"contrast\": (.6, .8)},\n",
"}\n",
"\n",
"\n",
"def draw_cond(cls, cond, rng):\n",
" kw = {}\n",
" for k, v in CONDITIONS[cond].items():\n",
" kw[k] = rng.uniform(*v) if isinstance(v, tuple) else v\n",
" return draw(cls, rng, **kw)\n",
"\n",
"\n",
"def make_dataset(n_per_class, seed, conds=(\"basic\",)):\n",
" \"\"\"(์ด๋ฏธ์ง ๋ฐฐ์ด N x 32 x 32, ์ ๋ต ๋ผ๋ฒจ, ์กฐ๊ฑด) ๋ฐํ. ์กฐ๊ฑด์ conds์์ ๋ฒ๊ฐ์ ๊ณ ๋ฅธ๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" X, y, c = [], [], []\n",
" for cls in CLASSES:\n",
" for i in range(n_per_class):\n",
" cond = conds[i % len(conds)]\n",
" X.append(draw_cond(cls, cond, rng)); y.append(cls); c.append(cond)\n",
" return np.array(X), np.array(y), np.array(c)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์ ์ฒ๋ฆฌ(๋ฐฐ์น ๋ฒกํฐํ)ยทํํ ํน์ง ๊ณตํต ํจ์ ----\n",
"OUT = 24 # ํฌ๊ธฐ ์ ๊ทํ ๊ฒฐ๊ณผ(24x24)\n",
"NOISE_T = 12.0 # ์ก์ ์ถ์ ๊ฐ์ด ์ด๋ณด๋ค ํฌ๋ฉด 3x3 ์ค๊ฐ๊ฐ ํํฐ ์ ์ฉ(์
์ฒํยท์ผ์ ์ก์)\n",
"FEATS = [\"elong\", \"fill\", \"solidity\", \"n_parts\", \"end_gap\", \"mid_bulge\", \"w_max\", \"compact\"]\n",
"\n",
"\n",
"def preprocess(images, noise_t=NOISE_T):\n",
" \"\"\"N์ฅ ์ผ๊ด ์ฒ๋ฆฌ: ๊ฒฐ์ธก ๋ณด์ -> ๊ทน์ฑ(์ผ๊ฐ ๋ฐ์ ) ๋ณด์ -> ์กฐ๊ฑด๋ณ ์ก์ ์ ๊ฑฐ -> ๋ช
์ ๋๋น ๊ฐํ -> ๋ถํ -> ์ค๊ณฝ์ -> ๋ฐฉํฅยทํฌ๊ธฐ ์ ๊ทํ.\"\"\"\n",
" f = np.array(images, dtype=float).reshape(-1, IMG, IMG)\n",
" n = len(f)\n",
" bad = ~np.isfinite(f)\n",
" missing = bad.sum(axis=(1, 2))\n",
" if bad.any(): # ๊ฒฐ์ธก ํฝ์
: ์์ ์ค์๊ฐ์ผ๋ก ์ฑ์ด ๋ค ์ฃผ๋ณ 3x3 ์ค๊ฐ๊ฐ์ผ๋ก ๋ณด์ \n",
" med = np.array([np.median(x[np.isfinite(x)]) if np.isfinite(x).any() else 128.0 for x in f])\n",
" f = np.where(bad, med[:, None, None], f)\n",
" f = np.where(bad, ndimage.median_filter(f, size=(1, 3, 3)), f)\n",
" border = np.concatenate([f[:, 0], f[:, -1], f[:, :, 0], f[:, :, -1]], axis=1)\n",
" inverted = np.median(border, axis=1) < 128 # ๋ฐฐ๊ฒฝ์ด ์ด๋์ฐ๋ฉด ์ผ๊ฐ ์ด์์ผ๋ก ๋ณด๊ณ ๋ฐ์ \n",
" f[inverted] = 255 - f[inverted]\n",
" med3 = ndimage.median_filter(f, size=(1, 3, 3))\n",
" noise = 1.4826 * np.median(np.abs(f - med3).reshape(n, -1), axis=1)\n",
" noisy = noise > noise_t\n",
" f[noisy] = med3[noisy]\n",
" lo, hi = np.percentile(f.reshape(n, -1), [1, 99], axis=1)\n",
" e = np.clip((f - lo[:, None, None]) / np.maximum(hi - lo, 1e-6)[:, None, None], 0, 1) # ๋ช
์ ๋๋น ๊ฐํ\n",
" mask = e < _otsu(e)[:, None, None]\n",
" mask &= ndimage.uniform_filter(mask.astype(float), size=(1, 3, 3)) * 9 >= 2.5 # ๊ณ ๋ฆฝ ์ ์ก์ ์ ๊ฑฐ\n",
" edge = np.hypot(ndimage.sobel(e, axis=1), ndimage.sobel(e, axis=2)) # ์ค๊ณฝ์ (Sobel)\n",
" ok = (mask.sum(axis=(1, 2)) >= 4) & (hi - lo > 8)\n",
" return {\"enh\": e, \"mask\": mask, \"edge\": edge / np.maximum(edge.max(axis=(1, 2)), 1e-6)[:, None, None],\n",
" \"norm\": _normalize(1 - e, mask, ok), \"inverted\": inverted, \"noise\": noise, \"denoised\": noisy,\n",
" \"missing\": missing, \"contrast\": hi - lo, \"ok\": ok}\n",
"\n",
"\n",
"def _otsu(e, bins=64):\n",
" n = len(e)\n",
" idx = np.minimum((e * bins).astype(int), bins - 1) + bins * np.arange(n)[:, None, None]\n",
" hist = np.bincount(idx.ravel(), minlength=bins * n).reshape(n, bins).astype(float)\n",
" centers = (np.arange(bins) + 0.5) / bins\n",
" w = np.cumsum(hist, axis=1); m = np.cumsum(hist * centers, axis=1)\n",
" tot, mt = w[:, -1:], m[:, -1:]\n",
" between = (mt * w / tot - m) ** 2 / np.maximum(w * (tot - w), 1)\n",
" return (np.argmax(between, axis=1) + 1) / bins\n",
"\n",
"\n",
"def _normalize(obj, mask, ok):\n",
" \"\"\"์ฃผ์ถ ๋ฐฉํฅ์ ์ธ๋ก๋ก ๋๋ฆฌ๊ณ ๋นํ์ฒด ์์ญ๋ง 24x24๋ก ๋ค์ ์ํ๋งํ๋ค(ํ์ ยทํฌ๊ธฐยท์์น ๋ถ๋ณ). ํ ๋ฒ์ ๋ณด๊ฐ์ผ๋ก ์ผ๊ด ์ฒ๋ฆฌ.\"\"\"\n",
" n = len(obj)\n",
" w = mask.astype(float)\n",
" m00 = np.maximum(w.sum(axis=(1, 2)), 1)\n",
" yy, xx = np.mgrid[0:IMG, 0:IMG].astype(float)\n",
" cy, cx = (w * yy).sum(axis=(1, 2)) / m00, (w * xx).sum(axis=(1, 2)) / m00\n",
" dy, dx = yy - cy[:, None, None], xx - cx[:, None, None]\n",
" mu20, mu02, mu11 = (w * dy * dy).sum(axis=(1, 2)), (w * dx * dx).sum(axis=(1, 2)), (w * dy * dx).sum(axis=(1, 2))\n",
" th = 0.5 * np.arctan2(2 * mu11, mu20 - mu02)\n",
" a = np.stack([np.cos(th), np.sin(th)], 1) # ์ฃผ์ถ(y, x)\n",
" b = np.stack([-np.sin(th), np.cos(th)], 1) # ๋ถ์ถ\n",
" pa = np.where(mask, dy * a[:, 0, None, None] + dx * a[:, 1, None, None], 0)\n",
" pb = np.where(mask, dy * b[:, 0, None, None] + dx * b[:, 1, None, None], 0)\n",
" half = np.maximum(np.abs(pa).max(axis=(1, 2)), np.abs(pb).max(axis=(1, 2))) + 1.0\n",
" g = np.linspace(-1, 1, OUT)\n",
" U, V = np.meshgrid(g, g, indexing=\"ij\")\n",
" Y = cy[:, None, None] + half[:, None, None] * (U * a[:, 0, None, None] + V * b[:, 0, None, None])\n",
" X = cx[:, None, None] + half[:, None, None] * (U * a[:, 1, None, None] + V * b[:, 1, None, None])\n",
" N = np.broadcast_to(np.arange(n)[:, None, None], Y.shape)\n",
" out = ndimage.map_coordinates(obj * mask, [N, Y, X], order=1, cval=0.0)\n",
" skew = (out * (U ** 3)).sum(axis=(1, 2))\n",
" out[skew < 0] = out[skew < 0, ::-1] # ๋ฌด๊ฒ๊ฐ ์ ๋ฆฐ ์ชฝ์ ์๋ก(180๋ ๋ชจํธ์ฑ ์ ๊ฑฐ)\n",
" out[~ok] = 0\n",
" return np.clip(out, 0, 1)\n",
"\n",
"\n",
"def shape_features(mask):\n",
" \"\"\"๋ถํ ๋ง์คํฌ 1์ฅ์์ ํ์ ์ ๋ฌด๊ดํ ํํ ํน์ง 8์ข
์ ๊ณ์ฐํ๋ค.\"\"\"\n",
" if mask.sum() < 4:\n",
" return dict.fromkeys(FEATS, 0.0)\n",
" yx = np.argwhere(mask).astype(float)\n",
" c = yx - yx.mean(axis=0)\n",
" val, vec = np.linalg.eigh(np.cov(c.T) + 1e-6 * np.eye(2))\n",
" t, s = c @ vec[:, 1], c @ vec[:, 0]\n",
" length, width = np.ptp(t) + 1, np.ptp(s) + 1\n",
" corners = np.vstack([yx + d for d in ([.5, .5], [.5, -.5], [-.5, .5], [-.5, -.5])])\n",
" try:\n",
" hull = ConvexHull(corners).volume\n",
" except Exception:\n",
" hull = float(mask.sum())\n",
" bins = np.digitize(t, np.linspace(t.min(), t.max() + 1e-6, 4)[1:-1])\n",
" w = np.array([np.ptp(s[bins == k]) + 1 if (bins == k).any() else 0 for k in range(3)]) / length\n",
" boundary = mask & ~ndimage.binary_erosion(mask)\n",
" return {\"elong\": float(np.sqrt(val[1] / max(val[0], 1e-6))), \"fill\": float(mask.sum() / (length * width)),\n",
" \"solidity\": float(mask.sum() / max(hull, 1)), \"n_parts\": float(ndimage.label(mask)[1]),\n",
" \"end_gap\": float(abs(w[0] - w[2])), \"mid_bulge\": float(w[1] - max(w[0], w[2])),\n",
" \"w_max\": float(w.max()), \"compact\": float(boundary.sum() / np.sqrt(mask.sum()))}\n",
"\n",
"\n",
"def feature_matrix(images):\n",
" \"\"\"์ ์ฒ๋ฆฌ ๊ฒฐ๊ณผ, ํํ ํน์ง ํ๋ ฌ(N x 8), ์ ๊ทํ ์ด๋ฏธ์ง ํ๋ ฌ(N x 576)์ ๋๋ ค์ค๋ค.\"\"\"\n",
" P = preprocess(images)\n",
" F = np.array([[d[k] for k in FEATS] for d in map(shape_features, P[\"mask\"])])\n",
" return P, F, P[\"norm\"].reshape(len(F), -1)\n",
"\n",
"warnings.filterwarnings(\"ignore\")\n",
"\n",
"# 1) ์ฒ๋ฆฌ ์๋: 2,400์ฅ ์ผ๊ด ์ฒ๋ฆฌ\n",
"X, y, c = make_dataset(400, 1, tuple(CONDITIONS))\n",
"preprocess(X[:50]) # ์ค๋น ์คํ\n",
"t0 = time.perf_counter()\n",
"P = preprocess(X)\n",
"dt = time.perf_counter() - t0\n",
"err = int((~np.isfinite(P[\"norm\"])).sum())\n",
"print(f\"์ ์ฒ๋ฆฌ {len(X):,}์ฅ, {len(X) / dt:,.0f}์ฅ/์ด (๋ชฉํ 1,000์ฅ/์ด ์ด์: {'๋ฌ์ฑ' if len(X) / dt >= 1000 else '๋ฏธ๋ฌ'}), ์ถ๋ ฅ ์ค๋ฅ {err}๊ฑด\")\n",
"\n",
"# 2) ๋จ์ ํ
์คํธ 5์ข
\n",
"tests = {}\n",
"tests[\"T1 ์ถ๋ ฅ ํฌ๊ธฐ 24x24 ๊ณ ์ \"] = P[\"norm\"].shape[1:] == (OUT, OUT)\n",
"tests[\"T2 ๊ฐ ๋ฒ์ 0~1\"] = bool(P[\"norm\"].min() >= 0 and P[\"norm\"].max() <= 1)\n",
"rng = np.random.default_rng(2)\n",
"day = draw(\"attack_fw\", rng, angle=30, scale=.8, fore=1.0, noise=0)\n",
"rng = np.random.default_rng(2)\n",
"night = draw(\"attack_fw\", rng, angle=30, scale=.8, fore=1.0, noise=0, invert=True)\n",
"pd_, pn = preprocess([day, night])[\"norm\"]\n",
"tests[\"T3 ์ผ๊ฐ ๋ฐ์ ์์ ๊ทน์ฑ ์๋ ๋ณด์ \"] = bool(np.abs(pd_ - pn).mean() < 0.05)\n",
"holes = day.astype(float); holes[rng.random(holes.shape) < 0.1] = np.nan\n",
"ph = preprocess([holes])\n",
"tests[\"T4 ๊ฒฐ์ธก ํฝ์
๋ณด์ ์จ 100%\"] = bool(ph[\"missing\"][0] > 0 and np.isfinite(ph[\"norm\"]).all())\n",
"blank = np.full((IMG, IMG), 200.0)\n",
"pb = preprocess([blank])\n",
"tests[\"T5 ๋น ์์์ ์์ธ ์์ด 'ํ๋ณ ๋ถ๊ฐ' ํ์\"] = bool(not pb[\"ok\"][0])\n",
"for k, v in tests.items():\n",
" print(f\"[{'PASS' if v else 'FAIL'}] {k}\")\n",
"print(f\"๊ฒฐ์ธก ํฝ์
{ph['missing'][0]}๊ฐ ๋ณด์ , ๋จ์ ํ
์คํธ {sum(tests.values())}/5 ํต๊ณผ\")\n",
"\n",
"# 3) ์ ์ฒ๋ฆฌ ์ ํ ์๋ณ ์ ํ๋(์ต๊ทผ์ ์ค์ฌ ๋ถ๋ฅ๊ธฐ, ๊ธฐ๋ณธ ์กฐ๊ฑด์ผ๋ก ํ์ต)\n",
"Xtr, ytr, _ = make_dataset(60, 3)\n",
"raw = lambda imgs: (np.asarray(imgs, float) / 255).reshape(len(imgs), -1)\n",
"nc_raw = NearestCentroid().fit(raw(Xtr), ytr)\n",
"nc_pre = NearestCentroid().fit(preprocess(Xtr)[\"norm\"].reshape(len(Xtr), -1), ytr)\n",
"rows = []\n",
"for cond in [\"basic\", \"offcenter\", \"night\", \"lowcon\", \"noise\", \"far\"]:\n",
" Xt, yt, _ = make_dataset(30, 4, (cond,))\n",
" pt = preprocess(Xt)\n",
" rows.append({\"์กฐ๊ฑด\": cond, \"์ ์ฒ๋ฆฌ ์ \": (nc_raw.predict(raw(Xt)) == yt).mean(),\n",
" \"์ ์ฒ๋ฆฌ ํ\": (nc_pre.predict(pt[\"norm\"].reshape(len(Xt), -1)) == yt).mean(),\n",
" \"์ก์ํํฐ ์ ์ฉ๋ฅ \": pt[\"denoised\"].mean(), \"๊ทน์ฑ๋ฐ์ ๋น์จ\": pt[\"inverted\"].mean()})\n",
"print(\"\\n[์ ์ฒ๋ฆฌ ์ ํ ์๋ณ ์ ํ๋]\")\n",
"print(pd.DataFrame(rows).round(3).to_string(index=False))\n",
"\n",
"# 4) ์กฐ๊ฑด๋ณ ์ก์ ์๊ณ๊ฐ ๋์ ์กฐ์ ํจ๊ณผ: ์๊ณ๊ฐ์ ๋ฎ์ถฐ ๋ชจ๋ ์์์ ํํฐ๋ฅผ ์ฐ๋ฉด ์๊ฑฐ๋ฆฌ ์ํ ํ์ ์ด ์์๋๋ค\n",
"Xf, yf, _ = make_dataset(30, 4, (\"far\",))\n",
"for t in (NOISE_T, 0.0):\n",
" acc = (nc_pre.predict(preprocess(Xf, noise_t=t)[\"norm\"].reshape(len(Xf), -1)) == yf).mean()\n",
" print(f\"์๊ฑฐ๋ฆฌ ์กฐ๊ฑด, ์ก์ ์๊ณ๊ฐ {t:>4}: ์ ํ๋ {acc:.3f}\")\n",
"\n",
"# 5) ์ ์ฒ๋ฆฌ ๋จ๊ณ ์๊ฐํ\n",
"idx = [np.flatnonzero(c == k)[0] for k in [\"basic\", \"night\", \"noise\", \"lowcon\", \"far\"]]\n",
"fig, ax = plt.subplots(4, len(idx), figsize=(10, 8))\n",
"for j, i in enumerate(idx):\n",
" for r, (img, name) in enumerate([(X[i], \"raw\"), (P[\"enh\"][i], \"contrast\"), (P[\"edge\"][i], \"edge\"), (P[\"norm\"][i], \"normalized 24x24\")]):\n",
" ax[r, j].imshow(img, cmap=\"gray\"); ax[r, j].axis(\"off\"); ax[r, j].set_title(f\"{c[i]} {name}\", fontsize=8)\n",
"plt.suptitle(\"Silhouette preprocessing steps (synthetic)\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
02 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"์ ์ฒ๋ฆฌ 2,400์ฅ, 2,056์ฅ/์ด (๋ชฉํ 1,000์ฅ/์ด ์ด์: ๋ฌ์ฑ), ์ถ๋ ฅ ์ค๋ฅ 0๊ฑด\n",
"[PASS] T1 ์ถ๋ ฅ ํฌ๊ธฐ 24x24 ๊ณ ์ \n",
"[PASS] T2 ๊ฐ ๋ฒ์ 0~1\n",
"[PASS] T3 ์ผ๊ฐ ๋ฐ์ ์์ ๊ทน์ฑ ์๋ ๋ณด์ \n",
"[PASS] T4 ๊ฒฐ์ธก ํฝ์
๋ณด์ ์จ 100%\n",
"[PASS] T5 ๋น ์์์ ์์ธ ์์ด 'ํ๋ณ ๋ถ๊ฐ' ํ์\n",
"๊ฒฐ์ธก ํฝ์
109๊ฐ ๋ณด์ , ๋จ์ ํ
์คํธ 5/5 ํต๊ณผ\n",
"\n",
"[์ ์ฒ๋ฆฌ ์ ํ ์๋ณ ์ ํ๋]\n",
" ์กฐ๊ฑด ์ ์ฒ๋ฆฌ ์ ์ ์ฒ๋ฆฌ ํ ์ก์ํํฐ ์ ์ฉ๋ฅ ๊ทน์ฑ๋ฐ์ ๋น์จ\n",
" basic 0.539 0.978 0.0 0.0\n",
"offcenter 0.344 0.939 0.0 0.0\n",
" night 0.167 0.983 0.0 1.0\n",
" lowcon 0.322 0.978 0.0 0.0\n",
" noise 0.544 0.911 1.0 0.0\n",
" far 0.194 0.722 0.0 0.0\n",
"์๊ฑฐ๋ฆฌ ์กฐ๊ฑด, ์ก์ ์๊ณ๊ฐ 12.0: ์ ํ๋ 0.722\n",
"์๊ฑฐ๋ฆฌ ์กฐ๊ฑด, ์ก์ ์๊ณ๊ฐ 0.0: ์ ํ๋ 0.417\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
03. ๊ธฐ๋ณธ ํํ ํน์ง ๊ธฐ๋ฐ 1์ฐจ ๋นํ์ฒด ์ค๋ฃจ์ฃ ๋ถ๋ฅ Baseline ๋ชจ๋ธ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๊ธฐ๋ณธ ํํ ํน์ง ๊ธฐ๋ฐ 1์ฐจ ๋นํ์ฒด ์ค๋ฃจ์ฃ ๋ถ๋ฅ Baseline ๋ชจ๋ธ์ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๊ธธ์ญํจยท์ฑ์๋ฅ ยท๋ณผ๋ก๋ยทํญ ๋ถํฌ ๊ฐ์ ๊ธฐ๋ณธ ํํ ํน์ง์ผ๋ก ๋นํ์ฒด ์ค๋ฃจ์ฃ์ 1์ฐจ ๋ถ๋ฅํ๋ Baseline ๋ชจ๋ธ์ ๊ตฌํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๋ถํ ๋ง์คํฌ์์ ํ์ ์ ๋ฌด๊ดํ ํํ ํน์ง 8์ข
์ ๊ณ์ฐํ๊ณ ๊ฒฐ์ ๋๋ฌด๋ก ๋ถ๋ฅ ๊ท์น์ ํ์ต ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/models/trn03_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": [
"\"\"\"ํํ ํน์ง 8์ข
(๊ธธ์ญํจยท์ฑ์๋ฅ ยท๋ณผ๋ก๋ยทํญ ๋ถํฌ ๋ฑ)์ผ๋ก ๋นํ์ฒด ์ค๋ฃจ์ฃ์ 1์ฐจ ๋ถ๋ฅํ๋ ๊ฒฐ์ ๋๋ฌด Baseline์ ๊ตฌํํ๊ณ , ํ๋จ ๊ท์น์ ๊ทธ๋๋ก ์ถ๋ ฅํด ํฌ๋ช
์ฑ์ ํ์ธํ๋ฉฐ ๊ธฐ๋ณธ ๋ฐ์ดํฐ์
์ ํ๋ยท์ฒ๋ฆฌ ์ง์ฐยท์กฐ๊ฑด๋ณ ํ๊ณ๋ฅผ ์ธก์ ํ๋ค.\"\"\"\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from matplotlib.path import Path as MplPath\n",
"from scipy import ndimage\n",
"from scipy.spatial import ConvexHull\n",
"from sklearn.tree import DecisionTreeClassifier, export_text, plot_tree\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ํฉ์ฑ ์ค๋ฃจ์ฃ ์์ฑ๊ธฐ (์ค์ ๊ธฐ์ฒด ์ฌ์งยท๋๋ฉดยท์ ์ ์ฌ์ฉ ์ ํจ)\n",
"# ์ฌ๊ฐํยท์ผ๊ฐํยท์ ๊ฐ์ ๊ธฐ๋ณธ ๋ํ์ ์กฐํฉํ ์ถ์ ์ค๋ฃจ์ฃ์ 32x32 ํ๋ฐฑ ์ด๋ฏธ์ง๋ก ๊ทธ๋ฆฐ๋ค.\n",
"# ํ์ (๋ค๊ฐ๋), ์๊ทผ ์ถ์(์ธก๋ฉด ์์ ), ํฌ๊ธฐ(์๊ฑฐ๋ฆฌ), ํ๋ฆผยท์ก์ยท์ ๋๋นยท๊ตฌ๋ฆยท๊ฐ๋ฆผยท์ผ๊ฐ ๋ฐ์ ์ ์กฐ์ ํ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"IMG = 32\n",
"CLASSES = [\"civil_quad\", \"civil_fw\", \"attack_fw\", \"cruise\", \"ballistic\", \"bird\"]\n",
"KOR = {\"civil_quad\": \"๋ฏผ๊ฐ ์ฟผ๋์ฝฅํฐํ\", \"civil_fw\": \"๋ฏผ๊ฐ ๊ณ ์ ์ตํ\", \"attack_fw\": \"์ผ๊ฐ์ต ์ํญ๋๋ก ํ\",\n",
" \"cruise\": \"์ํญ๋ฏธ์ฌ์ผํ\", \"ballistic\": \"ํ๋๋ฏธ์ฌ์ผํ\", \"bird\": \"์กฐ๋ฅ\"}\n",
"THREAT = {\"attack_fw\", \"cruise\", \"ballistic\"}\n",
"\n",
"\n",
"def _rect(x0, x1, y0, y1):\n",
" return [(x0, y0), (x1, y0), (x1, y1), (x0, y1)]\n",
"\n",
"\n",
"def _bar(p, q, w):\n",
" (x0, y0), (x1, y1) = p, q\n",
" n = np.array([y0 - y1, x1 - x0]) / np.hypot(x1 - x0, y1 - y0) * w / 2\n",
" return [(x0 + n[0], y0 + n[1]), (x1 + n[0], y1 + n[1]), (x1 - n[0], y1 - n[1]), (x0 - n[0], y0 - n[1])]\n",
"\n",
"\n",
"# ๋ํ ์ ์(๋จ์ ์ขํ, ๊ธฐ์ ๋ฐฉํฅ = +y). ๋ชจ๋ ์ถ์ ๋ํ์ด๋ฉฐ ํน์ ๊ธฐ์ข
์ ๋ณธ๋จ์ง ์๋๋ค.\n",
"SHAPES = {\n",
" \"civil_quad\": {\"poly\": [_bar((-.55, -.55), (.55, .55), .1), _bar((-.55, .55), (.55, -.55), .1), _rect(-.15, .15, -.15, .15)],\n",
" \"circ\": [(-.55, -.55, .24), (.55, .55, .24), (-.55, .55, .24), (.55, -.55, .24)]},\n",
" \"civil_fw\": {\"poly\": [_rect(-.07, .07, -.8, .72), _rect(-.9, .9, .05, .25), _rect(-.33, .33, -.8, -.66)], \"circ\": []},\n",
" \"attack_fw\": {\"poly\": [[(0, .85), (.85, -.5), (.12, -.42), (0, -.6), (-.12, -.42), (-.85, -.5)]], \"circ\": []},\n",
" \"cruise\": {\"poly\": [_rect(-.07, .07, -.9, .7), [(-.07, .7), (0, .92), (.07, .7)], _rect(-.42, .42, -.05, .07),\n",
" [(-.07, -.9), (-.24, -.95), (-.07, -.74)], [(.07, -.9), (.24, -.95), (.07, -.74)]], \"circ\": []},\n",
" \"ballistic\": {\"poly\": [_rect(-.13, .13, -.85, .6), [(-.13, .6), (0, .95), (.13, .6)],\n",
" [(-.13, -.85), (-.34, -.95), (-.13, -.58)], [(.13, -.85), (.34, -.95), (.13, -.58)]], \"circ\": []},\n",
" \"bird\": {\"poly\": [[(-.9, .12), (-.5, .28), (-.12, .06), (0, .22), (.12, .06), (.5, .28), (.9, .12), (.5, .12),\n",
" (.12, -.1), (0, -.38), (-.12, -.1), (-.5, .12)]], \"circ\": []},\n",
"}\n",
"_S = 2\n",
"_g = (np.arange(IMG * _S) + 0.5) / _S\n",
"_GX, _GY = np.meshgrid(_g, _g)\n",
"_PTS = np.c_[_GX.ravel(), _GY.ravel()]\n",
"_PATHS = {c: [MplPath(np.array(p)) for p in s[\"poly\"]] for c, s in SHAPES.items()}\n",
"\n",
"\n",
"def draw(cls, rng, angle=None, scale=None, fore=None, blur=0.3, noise=4.0, contrast=1.0,\n",
" clutter=0.0, occlude=0.0, invert=False, shift=1.5):\n",
" \"\"\"ํด๋์ค 1๊ฐ ์ค๋ฃจ์ฃ ์ด๋ฏธ์ง(uint8 32x32)๋ฅผ ๊ทธ๋ฆฐ๋ค. ๋ฐฐ๊ฒฝ์ ๋ฐ์ ํ๋, ๋นํ์ฒด๋ ์ด๋์ด ๊ทธ๋ฆผ์.\"\"\"\n",
" angle = rng.uniform(0, 360) if angle is None else angle\n",
" scale = rng.uniform(.7, .95) if scale is None else scale\n",
" fore = rng.uniform(.85, 1.0) if fore is None else fore\n",
" th = np.radians(angle)\n",
" c0 = IMG / 2 + rng.uniform(-shift, shift, 2)\n",
" d = (_PTS - c0) / (scale * IMG / 2) # ์ด๋ฏธ์ง ์ขํ -> ๋ํ ์ขํ(์ญ๋ณํ)\n",
" u = d[:, 0] * np.cos(th) + d[:, 1] * np.sin(th)\n",
" v = (-d[:, 0] * np.sin(th) + d[:, 1] * np.cos(th)) / fore # ์๊ทผ ์ถ์(๊ธฐ์ ๋ฐฉํฅ์ผ๋ก ์งง์ ๋ณด์)\n",
" uv = np.c_[u, -v]\n",
" m = np.zeros(len(uv), bool)\n",
" near = np.flatnonzero(np.abs(uv).max(axis=1) <= 1.0) # ๋ํ์ด ์์ ์ ์๋ ์์ญ๋ง ๊ฒ์ฌ(์๋)\n",
" for p in _PATHS[cls]:\n",
" m[near] |= p.contains_points(uv[near])\n",
" for cx, cy, r in SHAPES[cls][\"circ\"]:\n",
" m |= (uv[:, 0] - cx) ** 2 + (uv[:, 1] - cy) ** 2 <= r * r\n",
" cov = m.reshape(IMG, _S, IMG, _S).mean(axis=(1, 3))\n",
" if occlude > 0: # ๋ถ๋ถ ๊ฐ๋ฆผ(๊ตฌ๋ฆ ์กฐ๊ฐ)\n",
" h = int(IMG * np.sqrt(occlude)); y0, x0 = rng.integers(8, IMG - 8 - h // 2, 2)\n",
" cov[y0:y0 + h, x0:x0 + h] = 0\n",
" bg = rng.uniform(180, 220) + np.linspace(-10, 10, IMG)[:, None]\n",
" fg = rng.uniform(30, 60)\n",
" img = bg * (1 - cov) + fg * cov\n",
" if clutter > 0: # ๊ตฌ๋ฆยท๋ฐฐ๊ฒฝ ์ผ๋ฃฉ\n",
" img -= clutter * 90 * np.clip(ndimage.gaussian_filter(rng.normal(0, 1, (IMG, IMG)), 3) * 4, 0, None)\n",
" if blur > 0:\n",
" img = ndimage.gaussian_filter(img, blur)\n",
" mean = img.mean()\n",
" img = mean + (img - mean) * contrast\n",
" img = img + rng.normal(0, noise, img.shape)\n",
" if invert: # ์ผ๊ฐ ์ด์(IR) ๊ทน์ฑ ๋ฐ์ \n",
" img = 255 - img\n",
" return np.clip(img, 0, 255).astype(np.uint8)\n",
"\n",
"\n",
"# ์ดฌ์ ์กฐ๊ฑด ํ๋ฆฌ์
: ๊ธฐ๋ณธ + ๋ณตํฉ ์ํ ์๋๋ฆฌ์ค\n",
"CONDITIONS = {\n",
" \"basic\": {},\n",
" \"offcenter\": {\"shift\": 5.0, \"fore\": (.6, .85)},\n",
" \"far\": {\"scale\": (.32, .45), \"blur\": .6},\n",
" \"blur\": {\"blur\": (1.2, 1.8)},\n",
" \"lowcon\": {\"contrast\": (.2, .35)},\n",
" \"clutter\": {\"clutter\": .8},\n",
" \"occlude\": {\"occlude\": .08},\n",
" \"oblique\": {\"fore\": (.35, .6)},\n",
" \"night\": {\"invert\": True, \"noise\": 10.0},\n",
" \"noise\": {\"noise\": 28.0},\n",
" \"combo\": {\"scale\": (.4, .55), \"blur\": (.8, 1.1), \"noise\": 12.0, \"contrast\": (.6, .8)},\n",
"}\n",
"\n",
"\n",
"def draw_cond(cls, cond, rng):\n",
" kw = {}\n",
" for k, v in CONDITIONS[cond].items():\n",
" kw[k] = rng.uniform(*v) if isinstance(v, tuple) else v\n",
" return draw(cls, rng, **kw)\n",
"\n",
"\n",
"def make_dataset(n_per_class, seed, conds=(\"basic\",)):\n",
" \"\"\"(์ด๋ฏธ์ง ๋ฐฐ์ด N x 32 x 32, ์ ๋ต ๋ผ๋ฒจ, ์กฐ๊ฑด) ๋ฐํ. ์กฐ๊ฑด์ conds์์ ๋ฒ๊ฐ์ ๊ณ ๋ฅธ๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" X, y, c = [], [], []\n",
" for cls in CLASSES:\n",
" for i in range(n_per_class):\n",
" cond = conds[i % len(conds)]\n",
" X.append(draw_cond(cls, cond, rng)); y.append(cls); c.append(cond)\n",
" return np.array(X), np.array(y), np.array(c)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์ ์ฒ๋ฆฌ(๋ฐฐ์น ๋ฒกํฐํ)ยทํํ ํน์ง ๊ณตํต ํจ์ ----\n",
"OUT = 24 # ํฌ๊ธฐ ์ ๊ทํ ๊ฒฐ๊ณผ(24x24)\n",
"NOISE_T = 12.0 # ์ก์ ์ถ์ ๊ฐ์ด ์ด๋ณด๋ค ํฌ๋ฉด 3x3 ์ค๊ฐ๊ฐ ํํฐ ์ ์ฉ(์
์ฒํยท์ผ์ ์ก์)\n",
"FEATS = [\"elong\", \"fill\", \"solidity\", \"n_parts\", \"end_gap\", \"mid_bulge\", \"w_max\", \"compact\"]\n",
"\n",
"\n",
"def preprocess(images, noise_t=NOISE_T):\n",
" \"\"\"N์ฅ ์ผ๊ด ์ฒ๋ฆฌ: ๊ฒฐ์ธก ๋ณด์ -> ๊ทน์ฑ(์ผ๊ฐ ๋ฐ์ ) ๋ณด์ -> ์กฐ๊ฑด๋ณ ์ก์ ์ ๊ฑฐ -> ๋ช
์ ๋๋น ๊ฐํ -> ๋ถํ -> ์ค๊ณฝ์ -> ๋ฐฉํฅยทํฌ๊ธฐ ์ ๊ทํ.\"\"\"\n",
" f = np.array(images, dtype=float).reshape(-1, IMG, IMG)\n",
" n = len(f)\n",
" bad = ~np.isfinite(f)\n",
" missing = bad.sum(axis=(1, 2))\n",
" if bad.any(): # ๊ฒฐ์ธก ํฝ์
: ์์ ์ค์๊ฐ์ผ๋ก ์ฑ์ด ๋ค ์ฃผ๋ณ 3x3 ์ค๊ฐ๊ฐ์ผ๋ก ๋ณด์ \n",
" med = np.array([np.median(x[np.isfinite(x)]) if np.isfinite(x).any() else 128.0 for x in f])\n",
" f = np.where(bad, med[:, None, None], f)\n",
" f = np.where(bad, ndimage.median_filter(f, size=(1, 3, 3)), f)\n",
" border = np.concatenate([f[:, 0], f[:, -1], f[:, :, 0], f[:, :, -1]], axis=1)\n",
" inverted = np.median(border, axis=1) < 128 # ๋ฐฐ๊ฒฝ์ด ์ด๋์ฐ๋ฉด ์ผ๊ฐ ์ด์์ผ๋ก ๋ณด๊ณ ๋ฐ์ \n",
" f[inverted] = 255 - f[inverted]\n",
" med3 = ndimage.median_filter(f, size=(1, 3, 3))\n",
" noise = 1.4826 * np.median(np.abs(f - med3).reshape(n, -1), axis=1)\n",
" noisy = noise > noise_t\n",
" f[noisy] = med3[noisy]\n",
" lo, hi = np.percentile(f.reshape(n, -1), [1, 99], axis=1)\n",
" e = np.clip((f - lo[:, None, None]) / np.maximum(hi - lo, 1e-6)[:, None, None], 0, 1) # ๋ช
์ ๋๋น ๊ฐํ\n",
" mask = e < _otsu(e)[:, None, None]\n",
" mask &= ndimage.uniform_filter(mask.astype(float), size=(1, 3, 3)) * 9 >= 2.5 # ๊ณ ๋ฆฝ ์ ์ก์ ์ ๊ฑฐ\n",
" edge = np.hypot(ndimage.sobel(e, axis=1), ndimage.sobel(e, axis=2)) # ์ค๊ณฝ์ (Sobel)\n",
" ok = (mask.sum(axis=(1, 2)) >= 4) & (hi - lo > 8)\n",
" return {\"enh\": e, \"mask\": mask, \"edge\": edge / np.maximum(edge.max(axis=(1, 2)), 1e-6)[:, None, None],\n",
" \"norm\": _normalize(1 - e, mask, ok), \"inverted\": inverted, \"noise\": noise, \"denoised\": noisy,\n",
" \"missing\": missing, \"contrast\": hi - lo, \"ok\": ok}\n",
"\n",
"\n",
"def _otsu(e, bins=64):\n",
" n = len(e)\n",
" idx = np.minimum((e * bins).astype(int), bins - 1) + bins * np.arange(n)[:, None, None]\n",
" hist = np.bincount(idx.ravel(), minlength=bins * n).reshape(n, bins).astype(float)\n",
" centers = (np.arange(bins) + 0.5) / bins\n",
" w = np.cumsum(hist, axis=1); m = np.cumsum(hist * centers, axis=1)\n",
" tot, mt = w[:, -1:], m[:, -1:]\n",
" between = (mt * w / tot - m) ** 2 / np.maximum(w * (tot - w), 1)\n",
" return (np.argmax(between, axis=1) + 1) / bins\n",
"\n",
"\n",
"def _normalize(obj, mask, ok):\n",
" \"\"\"์ฃผ์ถ ๋ฐฉํฅ์ ์ธ๋ก๋ก ๋๋ฆฌ๊ณ ๋นํ์ฒด ์์ญ๋ง 24x24๋ก ๋ค์ ์ํ๋งํ๋ค(ํ์ ยทํฌ๊ธฐยท์์น ๋ถ๋ณ). ํ ๋ฒ์ ๋ณด๊ฐ์ผ๋ก ์ผ๊ด ์ฒ๋ฆฌ.\"\"\"\n",
" n = len(obj)\n",
" w = mask.astype(float)\n",
" m00 = np.maximum(w.sum(axis=(1, 2)), 1)\n",
" yy, xx = np.mgrid[0:IMG, 0:IMG].astype(float)\n",
" cy, cx = (w * yy).sum(axis=(1, 2)) / m00, (w * xx).sum(axis=(1, 2)) / m00\n",
" dy, dx = yy - cy[:, None, None], xx - cx[:, None, None]\n",
" mu20, mu02, mu11 = (w * dy * dy).sum(axis=(1, 2)), (w * dx * dx).sum(axis=(1, 2)), (w * dy * dx).sum(axis=(1, 2))\n",
" th = 0.5 * np.arctan2(2 * mu11, mu20 - mu02)\n",
" a = np.stack([np.cos(th), np.sin(th)], 1) # ์ฃผ์ถ(y, x)\n",
" b = np.stack([-np.sin(th), np.cos(th)], 1) # ๋ถ์ถ\n",
" pa = np.where(mask, dy * a[:, 0, None, None] + dx * a[:, 1, None, None], 0)\n",
" pb = np.where(mask, dy * b[:, 0, None, None] + dx * b[:, 1, None, None], 0)\n",
" half = np.maximum(np.abs(pa).max(axis=(1, 2)), np.abs(pb).max(axis=(1, 2))) + 1.0\n",
" g = np.linspace(-1, 1, OUT)\n",
" U, V = np.meshgrid(g, g, indexing=\"ij\")\n",
" Y = cy[:, None, None] + half[:, None, None] * (U * a[:, 0, None, None] + V * b[:, 0, None, None])\n",
" X = cx[:, None, None] + half[:, None, None] * (U * a[:, 1, None, None] + V * b[:, 1, None, None])\n",
" N = np.broadcast_to(np.arange(n)[:, None, None], Y.shape)\n",
" out = ndimage.map_coordinates(obj * mask, [N, Y, X], order=1, cval=0.0)\n",
" skew = (out * (U ** 3)).sum(axis=(1, 2))\n",
" out[skew < 0] = out[skew < 0, ::-1] # ๋ฌด๊ฒ๊ฐ ์ ๋ฆฐ ์ชฝ์ ์๋ก(180๋ ๋ชจํธ์ฑ ์ ๊ฑฐ)\n",
" out[~ok] = 0\n",
" return np.clip(out, 0, 1)\n",
"\n",
"\n",
"def shape_features(mask):\n",
" \"\"\"๋ถํ ๋ง์คํฌ 1์ฅ์์ ํ์ ์ ๋ฌด๊ดํ ํํ ํน์ง 8์ข
์ ๊ณ์ฐํ๋ค.\"\"\"\n",
" if mask.sum() < 4:\n",
" return dict.fromkeys(FEATS, 0.0)\n",
" yx = np.argwhere(mask).astype(float)\n",
" c = yx - yx.mean(axis=0)\n",
" val, vec = np.linalg.eigh(np.cov(c.T) + 1e-6 * np.eye(2))\n",
" t, s = c @ vec[:, 1], c @ vec[:, 0]\n",
" length, width = np.ptp(t) + 1, np.ptp(s) + 1\n",
" corners = np.vstack([yx + d for d in ([.5, .5], [.5, -.5], [-.5, .5], [-.5, -.5])])\n",
" try:\n",
" hull = ConvexHull(corners).volume\n",
" except Exception:\n",
" hull = float(mask.sum())\n",
" bins = np.digitize(t, np.linspace(t.min(), t.max() + 1e-6, 4)[1:-1])\n",
" w = np.array([np.ptp(s[bins == k]) + 1 if (bins == k).any() else 0 for k in range(3)]) / length\n",
" boundary = mask & ~ndimage.binary_erosion(mask)\n",
" return {\"elong\": float(np.sqrt(val[1] / max(val[0], 1e-6))), \"fill\": float(mask.sum() / (length * width)),\n",
" \"solidity\": float(mask.sum() / max(hull, 1)), \"n_parts\": float(ndimage.label(mask)[1]),\n",
" \"end_gap\": float(abs(w[0] - w[2])), \"mid_bulge\": float(w[1] - max(w[0], w[2])),\n",
" \"w_max\": float(w.max()), \"compact\": float(boundary.sum() / np.sqrt(mask.sum()))}\n",
"\n",
"\n",
"def feature_matrix(images):\n",
" \"\"\"์ ์ฒ๋ฆฌ ๊ฒฐ๊ณผ, ํํ ํน์ง ํ๋ ฌ(N x 8), ์ ๊ทํ ์ด๋ฏธ์ง ํ๋ ฌ(N x 576)์ ๋๋ ค์ค๋ค.\"\"\"\n",
" P = preprocess(images)\n",
" F = np.array([[d[k] for k in FEATS] for d in map(shape_features, P[\"mask\"])])\n",
" return P, F, P[\"norm\"].reshape(len(F), -1)\n",
"\n",
"# 1) ํ์ตยท๊ฒ์ฆ ๋ฐ์ดํฐ(๊ธฐ๋ณธ ์กฐ๊ฑด) ๋ถ๋ฆฌ\n",
"Xtr, ytr, _ = make_dataset(60, 1)\n",
"Xte, yte, _ = make_dataset(40, 2)\n",
"_, Ftr, _ = feature_matrix(Xtr)\n",
"_, Fte, _ = feature_matrix(Xte)\n",
"\n",
"# 2) ํด๋ฆฌ์คํฑ 1์ฐจ ํ๋: ๊ท์น ๊น์ด(์ค๋ช
๊ฐ๋ฅํ ๋ฒ์ 2~5)๋ณ ๊ฒ์ฆ ์ ํ๋\n",
"print(\"๊ท์น ๊น์ด ๊ฒ์ฆ ์ ํ๋\")\n",
"for d in [2, 3, 4, 5]:\n",
" m = DecisionTreeClassifier(max_depth=d, random_state=0).fit(Ftr, ytr)\n",
" print(f\"{d:>6} {(m.predict(Fte) == yte).mean():.3f}\")\n",
"model = DecisionTreeClassifier(max_depth=4, random_state=0).fit(Ftr, ytr)\n",
"\n",
"# 3) ๊ธฐ๋ณธ ๋ฐ์ดํฐ์
์ ํ๋์ ์ฒ๋ฆฌ ์ง์ฐ\n",
"t0 = time.perf_counter()\n",
"_, F1, _ = feature_matrix(Xte)\n",
"pred = model.predict(F1)\n",
"ms = (time.perf_counter() - t0) * 1000 / len(Xte)\n",
"acc = (pred == yte).mean()\n",
"print(f\"\\n๊ธฐ๋ณธ ๋ฐ์ดํฐ์
์ ํ๋ {acc:.1%} (๋ชฉํ 75% ์ด์: {'๋ฌ์ฑ' if acc >= 0.75 else '๋ฏธ๋ฌ'}), 1์ฅ๋น ํ๋จ ์ง์ฐ {ms:.2f} ms\")\n",
"print(pd.crosstab(pd.Series(yte, name=\"์ ๋ต\"), pd.Series(pred, name=\"์์ธก\")))\n",
"\n",
"# 4) ํ๋จ ๊ท์น(ํฌ๋ช
์ฑ): ๊ต๊ด์ด ๊ทธ๋๋ก ์ฝ์ ์ ์๋ if-then ๊ท์น\n",
"print(\"\\n[ํ๋จ ๊ท์น ์ผ๋ถ]\")\n",
"rules = export_text(model, feature_names=FEATS, decimals=2).splitlines()\n",
"print(\"\\n\".join(rules[:12]) + f\"\\n... (์ด {len(rules)}์ค)\")\n",
"imp = pd.Series(model.feature_importances_, index=FEATS).sort_values(ascending=False)\n",
"print(\"์ค์ ํน์ง ์์ 3:\", \", \".join(f\"{k}({v:.2f})\" for k, v in imp.head(3).items()))\n",
"\n",
"# 5) ํ๊ณ: ๊ธฐ๋ณธ ์กฐ๊ฑด์์ ๋ง๋ ๊ท์น์ ์ด๋ ค์ด ์กฐ๊ฑด์ ์ ์ฉ\n",
"rows = []\n",
"for cond in [\"far\", \"blur\", \"oblique\", \"occlude\", \"combo\"]:\n",
" Xh, yh, _ = make_dataset(20, 3, (cond,))\n",
" rows.append({\"์กฐ๊ฑด\": cond, \"์ ํ๋\": round((model.predict(feature_matrix(Xh)[1]) == yh).mean(), 3)})\n",
"print(\"\\n[์ด๋ ค์ด ์กฐ๊ฑด ์ ํ๋(Baseline ํ๊ณ)]\", \", \".join(f\"{r['์กฐ๊ฑด']} {r['์ ํ๋']}\" for r in rows))\n",
"print(\"๊ธฐ๋ณธ ์คํ ๋ก๊ทธ: ํ์ต 360์ฅ, ๊ฒ์ฆ 240์ฅ, ๊ท์น ๊น์ด 4, ์ค๋ฅ 0๊ฑด\")\n",
"\n",
"plt.figure(figsize=(14, 6))\n",
"plot_tree(model, feature_names=FEATS, class_names=list(model.classes_), filled=True, fontsize=7, max_depth=3)\n",
"plt.title(\"Baseline decision rules (synthetic silhouettes)\")\n",
"plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
03 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"๊ท์น ๊น์ด ๊ฒ์ฆ ์ ํ๋\n",
" 2 0.654\n",
" 3 0.812\n",
" 4 0.946\n",
" 5 0.979\n",
"\n",
"๊ธฐ๋ณธ ๋ฐ์ดํฐ์
์ ํ๋ 94.6% (๋ชฉํ 75% ์ด์: ๋ฌ์ฑ), 1์ฅ๋น ํ๋จ ์ง์ฐ 1.32 ms\n",
"์์ธก attack_fw ballistic bird civil_fw civil_quad cruise\n",
"์ ๋ต \n",
"attack_fw 40 0 0 0 0 0\n",
"ballistic 0 39 1 0 0 0\n",
"bird 0 2 38 0 0 0\n",
"civil_fw 0 0 0 40 0 0\n",
"civil_quad 0 0 0 8 32 0\n",
"cruise 0 0 2 0 0 38\n",
"\n",
"[ํ๋จ ๊ท์น ์ผ๋ถ]\n",
"|--- w_max <= 0.43\n",
"| |--- end_gap <= 0.10\n",
"| | |--- class: bird\n",
"| |--- end_gap > 0.10\n",
"| | |--- class: ballistic\n",
"|--- w_max > 0.43\n",
"| |--- fill <= 0.44\n",
"| | |--- w_max <= 0.64\n",
"| | | |--- class: cruise\n",
"| | |--- w_max > 0.64\n",
"| | | |--- fill <= 0.29\n",
"| | | | |--- class: civil_fw\n",
"... (์ด 16์ค)\n",
"์ค์ ํน์ง ์์ 3: w_max(0.41), fill(0.38), end_gap(0.21)\n",
"\n",
"[์ด๋ ค์ด ์กฐ๊ฑด ์ ํ๋(Baseline ํ๊ณ)] far 0.442, blur 0.45, oblique 0.292, occlude 0.533, combo 0.367\n",
"๊ธฐ๋ณธ ์คํ ๋ก๊ทธ: ํ์ต 360์ฅ, ๊ฒ์ฆ 240์ฅ, ๊ท์น ๊น์ด 4, ์ค๋ฅ 0๊ฑด\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
04. ๋ค์ํ ๊ฐ๋/๋ฐฐ๊ฒฝ ๋
ธ์ด์ฆ ํ๊ฒฝ ํ ์ ์ ๋๋ก ยท๋ฏธ์ฌ์ผ ์ ๋ฐ ์ด๋ฏธ์ง ๋ถ๋ฅ ๋ฅ๋ฌ๋ ๊ณ ๋ํ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๋ค์ํ ๊ฐ๋/๋ฐฐ๊ฒฝ ๋
ธ์ด์ฆ ํ๊ฒฝ ํ ์ ์ ๋๋ก ยท๋ฏธ์ฌ์ผ ์ ๋ฐ ์ด๋ฏธ์ง ๋ถ๋ฅ ๋ชจ๋ธ์ ๊ณ ๋ํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๋ค์ํ ๊ฐ๋ยท์๊ฑฐ๋ฆฌยท๋ฐฐ๊ฒฝ ์ก์ ํ๊ฒฝ์์๋ ์ค๋ฃจ์ฃ์ ์ ๋ฐํ๊ฒ ๋ถ๋ฅํ๋๋ก ์ํ ์ ๊ฒฝ๋ง ๋ชจ๋ธ๋ก ๊ณ ๋ํํ๊ณ ๋ณตํฉ ์ํ ์๋๋ฆฌ์ค๋ก ๊ฒ์ฆํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ ๊ทํ ์ด๋ฏธ์ง 576ํฝ์
๊ณผ ํํ ํน์ง 8์ข
์ ์
๋ ฅ์ผ๋ก ํ๋ ๋ค์ธต ํผ์
ํธ๋ก ์ ๋ชจ๋ ์ดฌ์ ์กฐ๊ฑด์ ์์ ์ฆ๊ฐ ๋ฐ์ดํฐ๋ก ํ์ต ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/models/trn03_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": [
"\"\"\"๋ค์ํ ๊ฐ๋ยท์๊ฑฐ๋ฆฌยทํ๋ฆผยท์ก์ยท๊ตฌ๋ฆยท๊ฐ๋ฆผยท์ผ๊ฐ ์กฐ๊ฑด์ ์์ด ํ์ตํ ๋ค์ธต ํผ์
ํธ๋ก (์ํ ์ ๊ฒฝ๋ง) ๋ถ๋ฅ๊ธฐ๋ก ์ค๋ฃจ์ฃ ๋ถ๋ฅ๋ฅผ ๊ณ ๋ํํ๊ณ , ๋ณตํฉ ์ํ ์๋๋ฆฌ์ค 10์ข
์์ ์ํ ์ฌํ์จยท์ฒ๋ฆฌ ์ง์ฐยท์ค๊ฒฝ๋ณด ์ ๋ฐ ํจํด์ ๊ฒ์ฆํ๋ค.\"\"\"\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from matplotlib.path import Path as MplPath\n",
"from scipy import ndimage\n",
"from scipy.spatial import ConvexHull\n",
"from sklearn.neural_network import MLPClassifier\n",
"from sklearn.preprocessing import StandardScaler\n",
"from sklearn.tree import DecisionTreeClassifier\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ํฉ์ฑ ์ค๋ฃจ์ฃ ์์ฑ๊ธฐ (์ค์ ๊ธฐ์ฒด ์ฌ์งยท๋๋ฉดยท์ ์ ์ฌ์ฉ ์ ํจ)\n",
"# ์ฌ๊ฐํยท์ผ๊ฐํยท์ ๊ฐ์ ๊ธฐ๋ณธ ๋ํ์ ์กฐํฉํ ์ถ์ ์ค๋ฃจ์ฃ์ 32x32 ํ๋ฐฑ ์ด๋ฏธ์ง๋ก ๊ทธ๋ฆฐ๋ค.\n",
"# ํ์ (๋ค๊ฐ๋), ์๊ทผ ์ถ์(์ธก๋ฉด ์์ ), ํฌ๊ธฐ(์๊ฑฐ๋ฆฌ), ํ๋ฆผยท์ก์ยท์ ๋๋นยท๊ตฌ๋ฆยท๊ฐ๋ฆผยท์ผ๊ฐ ๋ฐ์ ์ ์กฐ์ ํ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"IMG = 32\n",
"CLASSES = [\"civil_quad\", \"civil_fw\", \"attack_fw\", \"cruise\", \"ballistic\", \"bird\"]\n",
"KOR = {\"civil_quad\": \"๋ฏผ๊ฐ ์ฟผ๋์ฝฅํฐํ\", \"civil_fw\": \"๋ฏผ๊ฐ ๊ณ ์ ์ตํ\", \"attack_fw\": \"์ผ๊ฐ์ต ์ํญ๋๋ก ํ\",\n",
" \"cruise\": \"์ํญ๋ฏธ์ฌ์ผํ\", \"ballistic\": \"ํ๋๋ฏธ์ฌ์ผํ\", \"bird\": \"์กฐ๋ฅ\"}\n",
"THREAT = {\"attack_fw\", \"cruise\", \"ballistic\"}\n",
"\n",
"\n",
"def _rect(x0, x1, y0, y1):\n",
" return [(x0, y0), (x1, y0), (x1, y1), (x0, y1)]\n",
"\n",
"\n",
"def _bar(p, q, w):\n",
" (x0, y0), (x1, y1) = p, q\n",
" n = np.array([y0 - y1, x1 - x0]) / np.hypot(x1 - x0, y1 - y0) * w / 2\n",
" return [(x0 + n[0], y0 + n[1]), (x1 + n[0], y1 + n[1]), (x1 - n[0], y1 - n[1]), (x0 - n[0], y0 - n[1])]\n",
"\n",
"\n",
"# ๋ํ ์ ์(๋จ์ ์ขํ, ๊ธฐ์ ๋ฐฉํฅ = +y). ๋ชจ๋ ์ถ์ ๋ํ์ด๋ฉฐ ํน์ ๊ธฐ์ข
์ ๋ณธ๋จ์ง ์๋๋ค.\n",
"SHAPES = {\n",
" \"civil_quad\": {\"poly\": [_bar((-.55, -.55), (.55, .55), .1), _bar((-.55, .55), (.55, -.55), .1), _rect(-.15, .15, -.15, .15)],\n",
" \"circ\": [(-.55, -.55, .24), (.55, .55, .24), (-.55, .55, .24), (.55, -.55, .24)]},\n",
" \"civil_fw\": {\"poly\": [_rect(-.07, .07, -.8, .72), _rect(-.9, .9, .05, .25), _rect(-.33, .33, -.8, -.66)], \"circ\": []},\n",
" \"attack_fw\": {\"poly\": [[(0, .85), (.85, -.5), (.12, -.42), (0, -.6), (-.12, -.42), (-.85, -.5)]], \"circ\": []},\n",
" \"cruise\": {\"poly\": [_rect(-.07, .07, -.9, .7), [(-.07, .7), (0, .92), (.07, .7)], _rect(-.42, .42, -.05, .07),\n",
" [(-.07, -.9), (-.24, -.95), (-.07, -.74)], [(.07, -.9), (.24, -.95), (.07, -.74)]], \"circ\": []},\n",
" \"ballistic\": {\"poly\": [_rect(-.13, .13, -.85, .6), [(-.13, .6), (0, .95), (.13, .6)],\n",
" [(-.13, -.85), (-.34, -.95), (-.13, -.58)], [(.13, -.85), (.34, -.95), (.13, -.58)]], \"circ\": []},\n",
" \"bird\": {\"poly\": [[(-.9, .12), (-.5, .28), (-.12, .06), (0, .22), (.12, .06), (.5, .28), (.9, .12), (.5, .12),\n",
" (.12, -.1), (0, -.38), (-.12, -.1), (-.5, .12)]], \"circ\": []},\n",
"}\n",
"_S = 2\n",
"_g = (np.arange(IMG * _S) + 0.5) / _S\n",
"_GX, _GY = np.meshgrid(_g, _g)\n",
"_PTS = np.c_[_GX.ravel(), _GY.ravel()]\n",
"_PATHS = {c: [MplPath(np.array(p)) for p in s[\"poly\"]] for c, s in SHAPES.items()}\n",
"\n",
"\n",
"def draw(cls, rng, angle=None, scale=None, fore=None, blur=0.3, noise=4.0, contrast=1.0,\n",
" clutter=0.0, occlude=0.0, invert=False, shift=1.5):\n",
" \"\"\"ํด๋์ค 1๊ฐ ์ค๋ฃจ์ฃ ์ด๋ฏธ์ง(uint8 32x32)๋ฅผ ๊ทธ๋ฆฐ๋ค. ๋ฐฐ๊ฒฝ์ ๋ฐ์ ํ๋, ๋นํ์ฒด๋ ์ด๋์ด ๊ทธ๋ฆผ์.\"\"\"\n",
" angle = rng.uniform(0, 360) if angle is None else angle\n",
" scale = rng.uniform(.7, .95) if scale is None else scale\n",
" fore = rng.uniform(.85, 1.0) if fore is None else fore\n",
" th = np.radians(angle)\n",
" c0 = IMG / 2 + rng.uniform(-shift, shift, 2)\n",
" d = (_PTS - c0) / (scale * IMG / 2) # ์ด๋ฏธ์ง ์ขํ -> ๋ํ ์ขํ(์ญ๋ณํ)\n",
" u = d[:, 0] * np.cos(th) + d[:, 1] * np.sin(th)\n",
" v = (-d[:, 0] * np.sin(th) + d[:, 1] * np.cos(th)) / fore # ์๊ทผ ์ถ์(๊ธฐ์ ๋ฐฉํฅ์ผ๋ก ์งง์ ๋ณด์)\n",
" uv = np.c_[u, -v]\n",
" m = np.zeros(len(uv), bool)\n",
" near = np.flatnonzero(np.abs(uv).max(axis=1) <= 1.0) # ๋ํ์ด ์์ ์ ์๋ ์์ญ๋ง ๊ฒ์ฌ(์๋)\n",
" for p in _PATHS[cls]:\n",
" m[near] |= p.contains_points(uv[near])\n",
" for cx, cy, r in SHAPES[cls][\"circ\"]:\n",
" m |= (uv[:, 0] - cx) ** 2 + (uv[:, 1] - cy) ** 2 <= r * r\n",
" cov = m.reshape(IMG, _S, IMG, _S).mean(axis=(1, 3))\n",
" if occlude > 0: # ๋ถ๋ถ ๊ฐ๋ฆผ(๊ตฌ๋ฆ ์กฐ๊ฐ)\n",
" h = int(IMG * np.sqrt(occlude)); y0, x0 = rng.integers(8, IMG - 8 - h // 2, 2)\n",
" cov[y0:y0 + h, x0:x0 + h] = 0\n",
" bg = rng.uniform(180, 220) + np.linspace(-10, 10, IMG)[:, None]\n",
" fg = rng.uniform(30, 60)\n",
" img = bg * (1 - cov) + fg * cov\n",
" if clutter > 0: # ๊ตฌ๋ฆยท๋ฐฐ๊ฒฝ ์ผ๋ฃฉ\n",
" img -= clutter * 90 * np.clip(ndimage.gaussian_filter(rng.normal(0, 1, (IMG, IMG)), 3) * 4, 0, None)\n",
" if blur > 0:\n",
" img = ndimage.gaussian_filter(img, blur)\n",
" mean = img.mean()\n",
" img = mean + (img - mean) * contrast\n",
" img = img + rng.normal(0, noise, img.shape)\n",
" if invert: # ์ผ๊ฐ ์ด์(IR) ๊ทน์ฑ ๋ฐ์ \n",
" img = 255 - img\n",
" return np.clip(img, 0, 255).astype(np.uint8)\n",
"\n",
"\n",
"# ์ดฌ์ ์กฐ๊ฑด ํ๋ฆฌ์
: ๊ธฐ๋ณธ + ๋ณตํฉ ์ํ ์๋๋ฆฌ์ค\n",
"CONDITIONS = {\n",
" \"basic\": {},\n",
" \"offcenter\": {\"shift\": 5.0, \"fore\": (.6, .85)},\n",
" \"far\": {\"scale\": (.32, .45), \"blur\": .6},\n",
" \"blur\": {\"blur\": (1.2, 1.8)},\n",
" \"lowcon\": {\"contrast\": (.2, .35)},\n",
" \"clutter\": {\"clutter\": .8},\n",
" \"occlude\": {\"occlude\": .08},\n",
" \"oblique\": {\"fore\": (.35, .6)},\n",
" \"night\": {\"invert\": True, \"noise\": 10.0},\n",
" \"noise\": {\"noise\": 28.0},\n",
" \"combo\": {\"scale\": (.4, .55), \"blur\": (.8, 1.1), \"noise\": 12.0, \"contrast\": (.6, .8)},\n",
"}\n",
"\n",
"\n",
"def draw_cond(cls, cond, rng):\n",
" kw = {}\n",
" for k, v in CONDITIONS[cond].items():\n",
" kw[k] = rng.uniform(*v) if isinstance(v, tuple) else v\n",
" return draw(cls, rng, **kw)\n",
"\n",
"\n",
"def make_dataset(n_per_class, seed, conds=(\"basic\",)):\n",
" \"\"\"(์ด๋ฏธ์ง ๋ฐฐ์ด N x 32 x 32, ์ ๋ต ๋ผ๋ฒจ, ์กฐ๊ฑด) ๋ฐํ. ์กฐ๊ฑด์ conds์์ ๋ฒ๊ฐ์ ๊ณ ๋ฅธ๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" X, y, c = [], [], []\n",
" for cls in CLASSES:\n",
" for i in range(n_per_class):\n",
" cond = conds[i % len(conds)]\n",
" X.append(draw_cond(cls, cond, rng)); y.append(cls); c.append(cond)\n",
" return np.array(X), np.array(y), np.array(c)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์ ์ฒ๋ฆฌ(๋ฐฐ์น ๋ฒกํฐํ)ยทํํ ํน์ง ๊ณตํต ํจ์ ----\n",
"OUT = 24 # ํฌ๊ธฐ ์ ๊ทํ ๊ฒฐ๊ณผ(24x24)\n",
"NOISE_T = 12.0 # ์ก์ ์ถ์ ๊ฐ์ด ์ด๋ณด๋ค ํฌ๋ฉด 3x3 ์ค๊ฐ๊ฐ ํํฐ ์ ์ฉ(์
์ฒํยท์ผ์ ์ก์)\n",
"FEATS = [\"elong\", \"fill\", \"solidity\", \"n_parts\", \"end_gap\", \"mid_bulge\", \"w_max\", \"compact\"]\n",
"\n",
"\n",
"def preprocess(images, noise_t=NOISE_T):\n",
" \"\"\"N์ฅ ์ผ๊ด ์ฒ๋ฆฌ: ๊ฒฐ์ธก ๋ณด์ -> ๊ทน์ฑ(์ผ๊ฐ ๋ฐ์ ) ๋ณด์ -> ์กฐ๊ฑด๋ณ ์ก์ ์ ๊ฑฐ -> ๋ช
์ ๋๋น ๊ฐํ -> ๋ถํ -> ์ค๊ณฝ์ -> ๋ฐฉํฅยทํฌ๊ธฐ ์ ๊ทํ.\"\"\"\n",
" f = np.array(images, dtype=float).reshape(-1, IMG, IMG)\n",
" n = len(f)\n",
" bad = ~np.isfinite(f)\n",
" missing = bad.sum(axis=(1, 2))\n",
" if bad.any(): # ๊ฒฐ์ธก ํฝ์
: ์์ ์ค์๊ฐ์ผ๋ก ์ฑ์ด ๋ค ์ฃผ๋ณ 3x3 ์ค๊ฐ๊ฐ์ผ๋ก ๋ณด์ \n",
" med = np.array([np.median(x[np.isfinite(x)]) if np.isfinite(x).any() else 128.0 for x in f])\n",
" f = np.where(bad, med[:, None, None], f)\n",
" f = np.where(bad, ndimage.median_filter(f, size=(1, 3, 3)), f)\n",
" border = np.concatenate([f[:, 0], f[:, -1], f[:, :, 0], f[:, :, -1]], axis=1)\n",
" inverted = np.median(border, axis=1) < 128 # ๋ฐฐ๊ฒฝ์ด ์ด๋์ฐ๋ฉด ์ผ๊ฐ ์ด์์ผ๋ก ๋ณด๊ณ ๋ฐ์ \n",
" f[inverted] = 255 - f[inverted]\n",
" med3 = ndimage.median_filter(f, size=(1, 3, 3))\n",
" noise = 1.4826 * np.median(np.abs(f - med3).reshape(n, -1), axis=1)\n",
" noisy = noise > noise_t\n",
" f[noisy] = med3[noisy]\n",
" lo, hi = np.percentile(f.reshape(n, -1), [1, 99], axis=1)\n",
" e = np.clip((f - lo[:, None, None]) / np.maximum(hi - lo, 1e-6)[:, None, None], 0, 1) # ๋ช
์ ๋๋น ๊ฐํ\n",
" mask = e < _otsu(e)[:, None, None]\n",
" mask &= ndimage.uniform_filter(mask.astype(float), size=(1, 3, 3)) * 9 >= 2.5 # ๊ณ ๋ฆฝ ์ ์ก์ ์ ๊ฑฐ\n",
" edge = np.hypot(ndimage.sobel(e, axis=1), ndimage.sobel(e, axis=2)) # ์ค๊ณฝ์ (Sobel)\n",
" ok = (mask.sum(axis=(1, 2)) >= 4) & (hi - lo > 8)\n",
" return {\"enh\": e, \"mask\": mask, \"edge\": edge / np.maximum(edge.max(axis=(1, 2)), 1e-6)[:, None, None],\n",
" \"norm\": _normalize(1 - e, mask, ok), \"inverted\": inverted, \"noise\": noise, \"denoised\": noisy,\n",
" \"missing\": missing, \"contrast\": hi - lo, \"ok\": ok}\n",
"\n",
"\n",
"def _otsu(e, bins=64):\n",
" n = len(e)\n",
" idx = np.minimum((e * bins).astype(int), bins - 1) + bins * np.arange(n)[:, None, None]\n",
" hist = np.bincount(idx.ravel(), minlength=bins * n).reshape(n, bins).astype(float)\n",
" centers = (np.arange(bins) + 0.5) / bins\n",
" w = np.cumsum(hist, axis=1); m = np.cumsum(hist * centers, axis=1)\n",
" tot, mt = w[:, -1:], m[:, -1:]\n",
" between = (mt * w / tot - m) ** 2 / np.maximum(w * (tot - w), 1)\n",
" return (np.argmax(between, axis=1) + 1) / bins\n",
"\n",
"\n",
"def _normalize(obj, mask, ok):\n",
" \"\"\"์ฃผ์ถ ๋ฐฉํฅ์ ์ธ๋ก๋ก ๋๋ฆฌ๊ณ ๋นํ์ฒด ์์ญ๋ง 24x24๋ก ๋ค์ ์ํ๋งํ๋ค(ํ์ ยทํฌ๊ธฐยท์์น ๋ถ๋ณ). ํ ๋ฒ์ ๋ณด๊ฐ์ผ๋ก ์ผ๊ด ์ฒ๋ฆฌ.\"\"\"\n",
" n = len(obj)\n",
" w = mask.astype(float)\n",
" m00 = np.maximum(w.sum(axis=(1, 2)), 1)\n",
" yy, xx = np.mgrid[0:IMG, 0:IMG].astype(float)\n",
" cy, cx = (w * yy).sum(axis=(1, 2)) / m00, (w * xx).sum(axis=(1, 2)) / m00\n",
" dy, dx = yy - cy[:, None, None], xx - cx[:, None, None]\n",
" mu20, mu02, mu11 = (w * dy * dy).sum(axis=(1, 2)), (w * dx * dx).sum(axis=(1, 2)), (w * dy * dx).sum(axis=(1, 2))\n",
" th = 0.5 * np.arctan2(2 * mu11, mu20 - mu02)\n",
" a = np.stack([np.cos(th), np.sin(th)], 1) # ์ฃผ์ถ(y, x)\n",
" b = np.stack([-np.sin(th), np.cos(th)], 1) # ๋ถ์ถ\n",
" pa = np.where(mask, dy * a[:, 0, None, None] + dx * a[:, 1, None, None], 0)\n",
" pb = np.where(mask, dy * b[:, 0, None, None] + dx * b[:, 1, None, None], 0)\n",
" half = np.maximum(np.abs(pa).max(axis=(1, 2)), np.abs(pb).max(axis=(1, 2))) + 1.0\n",
" g = np.linspace(-1, 1, OUT)\n",
" U, V = np.meshgrid(g, g, indexing=\"ij\")\n",
" Y = cy[:, None, None] + half[:, None, None] * (U * a[:, 0, None, None] + V * b[:, 0, None, None])\n",
" X = cx[:, None, None] + half[:, None, None] * (U * a[:, 1, None, None] + V * b[:, 1, None, None])\n",
" N = np.broadcast_to(np.arange(n)[:, None, None], Y.shape)\n",
" out = ndimage.map_coordinates(obj * mask, [N, Y, X], order=1, cval=0.0)\n",
" skew = (out * (U ** 3)).sum(axis=(1, 2))\n",
" out[skew < 0] = out[skew < 0, ::-1] # ๋ฌด๊ฒ๊ฐ ์ ๋ฆฐ ์ชฝ์ ์๋ก(180๋ ๋ชจํธ์ฑ ์ ๊ฑฐ)\n",
" out[~ok] = 0\n",
" return np.clip(out, 0, 1)\n",
"\n",
"\n",
"def shape_features(mask):\n",
" \"\"\"๋ถํ ๋ง์คํฌ 1์ฅ์์ ํ์ ์ ๋ฌด๊ดํ ํํ ํน์ง 8์ข
์ ๊ณ์ฐํ๋ค.\"\"\"\n",
" if mask.sum() < 4:\n",
" return dict.fromkeys(FEATS, 0.0)\n",
" yx = np.argwhere(mask).astype(float)\n",
" c = yx - yx.mean(axis=0)\n",
" val, vec = np.linalg.eigh(np.cov(c.T) + 1e-6 * np.eye(2))\n",
" t, s = c @ vec[:, 1], c @ vec[:, 0]\n",
" length, width = np.ptp(t) + 1, np.ptp(s) + 1\n",
" corners = np.vstack([yx + d for d in ([.5, .5], [.5, -.5], [-.5, .5], [-.5, -.5])])\n",
" try:\n",
" hull = ConvexHull(corners).volume\n",
" except Exception:\n",
" hull = float(mask.sum())\n",
" bins = np.digitize(t, np.linspace(t.min(), t.max() + 1e-6, 4)[1:-1])\n",
" w = np.array([np.ptp(s[bins == k]) + 1 if (bins == k).any() else 0 for k in range(3)]) / length\n",
" boundary = mask & ~ndimage.binary_erosion(mask)\n",
" return {\"elong\": float(np.sqrt(val[1] / max(val[0], 1e-6))), \"fill\": float(mask.sum() / (length * width)),\n",
" \"solidity\": float(mask.sum() / max(hull, 1)), \"n_parts\": float(ndimage.label(mask)[1]),\n",
" \"end_gap\": float(abs(w[0] - w[2])), \"mid_bulge\": float(w[1] - max(w[0], w[2])),\n",
" \"w_max\": float(w.max()), \"compact\": float(boundary.sum() / np.sqrt(mask.sum()))}\n",
"\n",
"\n",
"def feature_matrix(images):\n",
" \"\"\"์ ์ฒ๋ฆฌ ๊ฒฐ๊ณผ, ํํ ํน์ง ํ๋ ฌ(N x 8), ์ ๊ทํ ์ด๋ฏธ์ง ํ๋ ฌ(N x 576)์ ๋๋ ค์ค๋ค.\"\"\"\n",
" P = preprocess(images)\n",
" F = np.array([[d[k] for k in FEATS] for d in map(shape_features, P[\"mask\"])])\n",
" return P, F, P[\"norm\"].reshape(len(F), -1)\n",
"\n",
"# ---- ๋ถ๋ฅ ๋ชจ๋ธ ๊ณตํต ํจ์(๋ค์ธต ํผ์
ํธ๋ก : ์ ๊ทํ ์ด๋ฏธ์ง 576ํฝ์
+ ํํ ํน์ง 8์ข
) ----\n",
"def to_input(F, N, scaler):\n",
" return np.c_[N, scaler.transform(F) * 0.3]\n",
"\n",
"\n",
"def train_model(n_per_class=150, seed=5, conds=tuple(CONDITIONS), hidden=(64,)):\n",
" X, y, _ = make_dataset(n_per_class, seed, conds)\n",
" P, F, N = feature_matrix(X)\n",
" scaler = StandardScaler().fit(F)\n",
" model = MLPClassifier(hidden, max_iter=200, early_stopping=True, random_state=0)\n",
" model.fit(to_input(F, N, scaler), [CLASSES.index(v) for v in y])\n",
" return model, scaler\n",
"\n",
"\n",
"def predict(model, scaler, images):\n",
" \"\"\"(์ ์ฒ๋ฆฌ ๊ฒฐ๊ณผ, ํด๋์ค๋ณ ํ๋ฅ N x 6)์ ๋๋ ค์ค๋ค.\"\"\"\n",
" P, F, N = feature_matrix(images)\n",
" return P, model.predict_proba(to_input(F, N, scaler))\n",
"\n",
"# 1) ํ์ต: ๋ชจ๋ ์ดฌ์ ์กฐ๊ฑด์ ์์ ์ฆ๊ฐ ๋ฐ์ดํฐ(ํด๋์ค๋น 200์ฅ)\n",
"t0 = time.perf_counter()\n",
"model, scaler = train_model(200, seed=5, hidden=(96,))\n",
"print(f\"์ ๊ฒฝ๋ง ํ์ต ์๋ฃ ({time.perf_counter() - t0:.1f} s, ์
๋ ฅ 584์ฐจ์ = ์ ๊ทํ ์ด๋ฏธ์ง 576 + ํํ ํน์ง 8)\")\n",
"Xb, yb, _ = make_dataset(60, 1)\n",
"tree = DecisionTreeClassifier(max_depth=4, random_state=0).fit(feature_matrix(Xb)[1], yb) # ๋ฏธ์
3 Baseline\n",
"\n",
"# 2) ๋ณตํฉ ์ํ ์๋๋ฆฌ์ค 10์ข
์ํ(์๋๋ฆฌ์ค๋น ํด๋์ค๋ณ 15์ฅ)\n",
"SCEN = [k for k in CONDITIONS if k != \"basic\"]\n",
"rows, all_true, all_pred = [], [], []\n",
"for i, cond in enumerate(SCEN):\n",
" X, y, _ = make_dataset(15, 100 + i, (cond,))\n",
" t0 = time.perf_counter()\n",
" P, prob = predict(model, scaler, X)\n",
" ms = (time.perf_counter() - t0) * 1000 / len(X)\n",
" pred = np.array(CLASSES)[prob.argmax(1)]\n",
" thr = np.isin(y, list(THREAT))\n",
" rec = np.isin(pred[thr], list(THREAT)).mean()\n",
" rows.append({\"์๋๋ฆฌ์ค\": cond, \"Baseline ์ ํ๋\": (tree.predict(feature_matrix(X)[1]) == y).mean(), \"์ ๊ฒฝ๋ง ์ ํ๋\": (pred == y).mean(),\n",
" \"์ํ ์ฌํ์จ\": rec, \"์ฅ๋น ms\": ms, \"ํต๊ณผ\": \"PASS\" if rec >= 0.85 and ms <= 200 else \"FAIL\"})\n",
" all_true += list(y); all_pred += list(pred)\n",
"df = pd.DataFrame(rows)\n",
"print(df.round(3).to_string(index=False))\n",
"yt, yp = np.array(all_true), np.array(all_pred)\n",
"tr = np.isin(yt, list(THREAT))\n",
"recall = np.isin(yp[tr], list(THREAT)).mean()\n",
"print(f\"\\n๋ณตํฉ ์ํ ์ฌํ์จ {recall:.1%} (๋ชฉํ 85% ์ด์: {'๋ฌ์ฑ' if recall >= 0.85 else '๋ฏธ๋ฌ'}), ์ต๋ ์ฒ๋ฆฌ ์ง์ฐ {df['์ฅ๋น ms'].max():.2f} ms (๋ชฉํ 200 ms ์ดํ)\")\n",
"print(f\"์๋๋ฆฌ์ค ํต๊ณผ {int((df['ํต๊ณผ'] == 'PASS').sum())}/10\")\n",
"\n",
"# 3) ์ค๊ฒฝ๋ณด ์ ๋ฐ ํจํด: ๋น์ํ(๋ฏผ๊ฐ๊ธฐยท์กฐ๋ฅ)์ ์ํ์ผ๋ก ํ์ ํ ๊ฒฝ์ฐ\n",
"fa = pd.Series([f\"{t} -> {p}\" for t, p in zip(yt, yp) if t not in THREAT and p in THREAT])\n",
"print(\"\\n[์ค๊ฒฝ๋ณด ์ ๋ฐ ํจํด ์์]\", fa.value_counts().head(3).to_dict())\n",
"miss = pd.Series([f\"{t} -> {p}\" for t, p in zip(yt, yp) if t in THREAT and p not in THREAT])\n",
"print(\"[์ํ ๋ฏธํ ํจํด ์์]\", miss.value_counts().head(3).to_dict())\n",
"print(\"ํน์ง ๊ณต๊ฐ ์ฌ์ค๊ณ ๋ฐฉํฅ: ์๊ฑฐ๋ฆฌยท๊ฒฐํฉ ์กฐ๊ฑด์์ ๋ ๊ฐ ๋ ํ์์ด ๋ญ๊ฐ์ง๋ฏ๋ก ํญ ๋ถํฌ(end_gap, w_max) ์ธ์ ์ค๊ณฝ์ ๊ณก๋ฅ ํน์ง ์ถ๊ฐ๋ฅผ ๊ฒํ ํ๋ค.\")\n",
"\n",
"plt.figure(figsize=(9, 4))\n",
"x = np.arange(len(df))\n",
"plt.bar(x - 0.2, df[\"Baseline ์ ํ๋\"], 0.4, label=\"baseline tree\")\n",
"plt.bar(x + 0.2, df[\"์ ๊ฒฝ๋ง ์ ํ๋\"], 0.4, label=\"MLP (augmented)\")\n",
"plt.axhline(0.85, ls=\"--\", c=\"gray\")\n",
"plt.xticks(x, df[\"์๋๋ฆฌ์ค\"], rotation=30); plt.ylabel(\"accuracy\"); plt.legend()\n",
"plt.title(\"Accuracy by complex scenario (synthetic)\"); plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
04 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"์ ๊ฒฝ๋ง ํ์ต ์๋ฃ (3.3 s, ์
๋ ฅ 584์ฐจ์ = ์ ๊ทํ ์ด๋ฏธ์ง 576 + ํํ ํน์ง 8)\n",
" ์๋๋ฆฌ์ค Baseline ์ ํ๋ ์ ๊ฒฝ๋ง ์ ํ๋ ์ํ ์ฌํ์จ ์ฅ๋น ms ํต๊ณผ\n",
"offcenter 0.767 1.000 1.000 1.239 PASS\n",
" far 0.467 0.922 0.911 1.131 PASS\n",
" blur 0.400 1.000 1.000 1.201 PASS\n",
" lowcon 0.900 1.000 1.000 1.735 PASS\n",
" clutter 0.556 0.700 0.711 1.153 FAIL\n",
" occlude 0.556 0.789 0.844 1.234 FAIL\n",
" oblique 0.333 0.978 1.000 1.544 PASS\n",
" night 0.967 1.000 1.000 1.207 PASS\n",
" noise 0.778 0.956 1.000 1.178 PASS\n",
" combo 0.311 0.378 0.600 1.319 FAIL\n",
"\n",
"๋ณตํฉ ์ํ ์ฌํ์จ 90.7% (๋ชฉํ 85% ์ด์: ๋ฌ์ฑ), ์ต๋ ์ฒ๋ฆฌ ์ง์ฐ 1.73 ms (๋ชฉํ 200 ms ์ดํ)\n",
"์๋๋ฆฌ์ค ํต๊ณผ 7/10\n",
"\n",
"[์ค๊ฒฝ๋ณด ์ ๋ฐ ํจํด ์์] {'bird -> cruise': 9, 'civil_fw -> cruise': 7, 'civil_quad -> cruise': 6}\n",
"[์ํ ๋ฏธํ ํจํด ์์] {'cruise -> bird': 14, 'ballistic -> bird': 11, 'ballistic -> civil_fw': 8}\n",
"ํน์ง ๊ณต๊ฐ ์ฌ์ค๊ณ ๋ฐฉํฅ: ์๊ฑฐ๋ฆฌยท๊ฒฐํฉ ์กฐ๊ฑด์์ ๋ ๊ฐ ๋ ํ์์ด ๋ญ๊ฐ์ง๋ฏ๋ก ํญ ๋ถํฌ(end_gap, w_max) ์ธ์ ์ค๊ณฝ์ ๊ณก๋ฅ ํน์ง ์ถ๊ฐ๋ฅผ ๊ฒํ ํ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
05. ์ ์ฌ ํ์ ๋นํ์ฒด(๋ฏผ๊ฐ ๋๋ก vs ์ํญ ๋๋ก ) ์ค๋ถ๋ฅ ๋ฐฉ์ง ์ ๋ขฐ๋ ์ ์ ์์ง ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ ์ฌ ํ์ ๋นํ์ฒด ์ค๋ถ๋ฅ ๋ฐฉ์ง ์ ๋ขฐ๋ ์ ์ ์ฐ์ถ ์์ง์ ๊ฐ๋ฐํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๋ฏผ๊ฐ ๋๋ก ๊ณผ ์ํญ ๋๋ก ์ฒ๋ผ ํ์์ด ๋น์ทํ ๋นํ์ฒด์ ์ค๋ถ๋ฅ๋ฅผ ๋ง๊ธฐ ์ํด ์ ๋ขฐ๋ ์ ์์ ๋ฑ๊ธ์ ์ฐ์ถํ๋ ์์ง์ ๊ฐ๋ฐํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๋ถ๋ฅ ํ๋ฅ , 1ยท2์ ํ๋ฅ ์ฐจ์ด, ์์ ํ์ง(๋๋นยทํฌ๊ธฐยท์ก์)์ ๊ฐ์ค ๊ฒฐํฉํด ์ ๋ขฐ๋ ์ ์๋ฅผ ๋ง๋ค๊ณ HighยทMediumยทLow ๋ฑ๊ธ์ ๋ถ์ฌ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/verification/trn03_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": [
"\"\"\"๋ฏผ๊ฐ ๋๋ก ๊ณผ ์ํญ ๋๋ก ์ฒ๋ผ ํ์์ด ๋น์ทํ ๋นํ์ฒด์ ์ค๋ถ๋ฅ๋ฅผ ์ค์ด๊ธฐ ์ํด, ๋ถ๋ฅ ํ๋ฅ ยท1/2์ ํ๋ฅ ์ฐจ์ดยท์์ ํ์ง์ ๊ฒฐํฉํ ์ ๋ขฐ๋ ์ ์์ ๋ฑ๊ธ(High/Medium/Low)์ ์ฐ์ถํ๊ณ , ์ค๊ฒฝ๋ณด์จ(FAR) ๊ฐ์ถยท์ ์ ์ผ๊ด์ฑยทROC-AUCยท์๊ณ๊ฐ ๋ฏผ๊ฐ๋๋ฅผ ๊ฒ์ฆํ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from matplotlib.path import Path as MplPath\n",
"from scipy import ndimage\n",
"from scipy.spatial import ConvexHull\n",
"from sklearn.metrics import roc_auc_score, roc_curve\n",
"from sklearn.neural_network import MLPClassifier\n",
"from sklearn.preprocessing import StandardScaler\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ํฉ์ฑ ์ค๋ฃจ์ฃ ์์ฑ๊ธฐ (์ค์ ๊ธฐ์ฒด ์ฌ์งยท๋๋ฉดยท์ ์ ์ฌ์ฉ ์ ํจ)\n",
"# ์ฌ๊ฐํยท์ผ๊ฐํยท์ ๊ฐ์ ๊ธฐ๋ณธ ๋ํ์ ์กฐํฉํ ์ถ์ ์ค๋ฃจ์ฃ์ 32x32 ํ๋ฐฑ ์ด๋ฏธ์ง๋ก ๊ทธ๋ฆฐ๋ค.\n",
"# ํ์ (๋ค๊ฐ๋), ์๊ทผ ์ถ์(์ธก๋ฉด ์์ ), ํฌ๊ธฐ(์๊ฑฐ๋ฆฌ), ํ๋ฆผยท์ก์ยท์ ๋๋นยท๊ตฌ๋ฆยท๊ฐ๋ฆผยท์ผ๊ฐ ๋ฐ์ ์ ์กฐ์ ํ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"IMG = 32\n",
"CLASSES = [\"civil_quad\", \"civil_fw\", \"attack_fw\", \"cruise\", \"ballistic\", \"bird\"]\n",
"KOR = {\"civil_quad\": \"๋ฏผ๊ฐ ์ฟผ๋์ฝฅํฐํ\", \"civil_fw\": \"๋ฏผ๊ฐ ๊ณ ์ ์ตํ\", \"attack_fw\": \"์ผ๊ฐ์ต ์ํญ๋๋ก ํ\",\n",
" \"cruise\": \"์ํญ๋ฏธ์ฌ์ผํ\", \"ballistic\": \"ํ๋๋ฏธ์ฌ์ผํ\", \"bird\": \"์กฐ๋ฅ\"}\n",
"THREAT = {\"attack_fw\", \"cruise\", \"ballistic\"}\n",
"\n",
"\n",
"def _rect(x0, x1, y0, y1):\n",
" return [(x0, y0), (x1, y0), (x1, y1), (x0, y1)]\n",
"\n",
"\n",
"def _bar(p, q, w):\n",
" (x0, y0), (x1, y1) = p, q\n",
" n = np.array([y0 - y1, x1 - x0]) / np.hypot(x1 - x0, y1 - y0) * w / 2\n",
" return [(x0 + n[0], y0 + n[1]), (x1 + n[0], y1 + n[1]), (x1 - n[0], y1 - n[1]), (x0 - n[0], y0 - n[1])]\n",
"\n",
"\n",
"# ๋ํ ์ ์(๋จ์ ์ขํ, ๊ธฐ์ ๋ฐฉํฅ = +y). ๋ชจ๋ ์ถ์ ๋ํ์ด๋ฉฐ ํน์ ๊ธฐ์ข
์ ๋ณธ๋จ์ง ์๋๋ค.\n",
"SHAPES = {\n",
" \"civil_quad\": {\"poly\": [_bar((-.55, -.55), (.55, .55), .1), _bar((-.55, .55), (.55, -.55), .1), _rect(-.15, .15, -.15, .15)],\n",
" \"circ\": [(-.55, -.55, .24), (.55, .55, .24), (-.55, .55, .24), (.55, -.55, .24)]},\n",
" \"civil_fw\": {\"poly\": [_rect(-.07, .07, -.8, .72), _rect(-.9, .9, .05, .25), _rect(-.33, .33, -.8, -.66)], \"circ\": []},\n",
" \"attack_fw\": {\"poly\": [[(0, .85), (.85, -.5), (.12, -.42), (0, -.6), (-.12, -.42), (-.85, -.5)]], \"circ\": []},\n",
" \"cruise\": {\"poly\": [_rect(-.07, .07, -.9, .7), [(-.07, .7), (0, .92), (.07, .7)], _rect(-.42, .42, -.05, .07),\n",
" [(-.07, -.9), (-.24, -.95), (-.07, -.74)], [(.07, -.9), (.24, -.95), (.07, -.74)]], \"circ\": []},\n",
" \"ballistic\": {\"poly\": [_rect(-.13, .13, -.85, .6), [(-.13, .6), (0, .95), (.13, .6)],\n",
" [(-.13, -.85), (-.34, -.95), (-.13, -.58)], [(.13, -.85), (.34, -.95), (.13, -.58)]], \"circ\": []},\n",
" \"bird\": {\"poly\": [[(-.9, .12), (-.5, .28), (-.12, .06), (0, .22), (.12, .06), (.5, .28), (.9, .12), (.5, .12),\n",
" (.12, -.1), (0, -.38), (-.12, -.1), (-.5, .12)]], \"circ\": []},\n",
"}\n",
"_S = 2\n",
"_g = (np.arange(IMG * _S) + 0.5) / _S\n",
"_GX, _GY = np.meshgrid(_g, _g)\n",
"_PTS = np.c_[_GX.ravel(), _GY.ravel()]\n",
"_PATHS = {c: [MplPath(np.array(p)) for p in s[\"poly\"]] for c, s in SHAPES.items()}\n",
"\n",
"\n",
"def draw(cls, rng, angle=None, scale=None, fore=None, blur=0.3, noise=4.0, contrast=1.0,\n",
" clutter=0.0, occlude=0.0, invert=False, shift=1.5):\n",
" \"\"\"ํด๋์ค 1๊ฐ ์ค๋ฃจ์ฃ ์ด๋ฏธ์ง(uint8 32x32)๋ฅผ ๊ทธ๋ฆฐ๋ค. ๋ฐฐ๊ฒฝ์ ๋ฐ์ ํ๋, ๋นํ์ฒด๋ ์ด๋์ด ๊ทธ๋ฆผ์.\"\"\"\n",
" angle = rng.uniform(0, 360) if angle is None else angle\n",
" scale = rng.uniform(.7, .95) if scale is None else scale\n",
" fore = rng.uniform(.85, 1.0) if fore is None else fore\n",
" th = np.radians(angle)\n",
" c0 = IMG / 2 + rng.uniform(-shift, shift, 2)\n",
" d = (_PTS - c0) / (scale * IMG / 2) # ์ด๋ฏธ์ง ์ขํ -> ๋ํ ์ขํ(์ญ๋ณํ)\n",
" u = d[:, 0] * np.cos(th) + d[:, 1] * np.sin(th)\n",
" v = (-d[:, 0] * np.sin(th) + d[:, 1] * np.cos(th)) / fore # ์๊ทผ ์ถ์(๊ธฐ์ ๋ฐฉํฅ์ผ๋ก ์งง์ ๋ณด์)\n",
" uv = np.c_[u, -v]\n",
" m = np.zeros(len(uv), bool)\n",
" near = np.flatnonzero(np.abs(uv).max(axis=1) <= 1.0) # ๋ํ์ด ์์ ์ ์๋ ์์ญ๋ง ๊ฒ์ฌ(์๋)\n",
" for p in _PATHS[cls]:\n",
" m[near] |= p.contains_points(uv[near])\n",
" for cx, cy, r in SHAPES[cls][\"circ\"]:\n",
" m |= (uv[:, 0] - cx) ** 2 + (uv[:, 1] - cy) ** 2 <= r * r\n",
" cov = m.reshape(IMG, _S, IMG, _S).mean(axis=(1, 3))\n",
" if occlude > 0: # ๋ถ๋ถ ๊ฐ๋ฆผ(๊ตฌ๋ฆ ์กฐ๊ฐ)\n",
" h = int(IMG * np.sqrt(occlude)); y0, x0 = rng.integers(8, IMG - 8 - h // 2, 2)\n",
" cov[y0:y0 + h, x0:x0 + h] = 0\n",
" bg = rng.uniform(180, 220) + np.linspace(-10, 10, IMG)[:, None]\n",
" fg = rng.uniform(30, 60)\n",
" img = bg * (1 - cov) + fg * cov\n",
" if clutter > 0: # ๊ตฌ๋ฆยท๋ฐฐ๊ฒฝ ์ผ๋ฃฉ\n",
" img -= clutter * 90 * np.clip(ndimage.gaussian_filter(rng.normal(0, 1, (IMG, IMG)), 3) * 4, 0, None)\n",
" if blur > 0:\n",
" img = ndimage.gaussian_filter(img, blur)\n",
" mean = img.mean()\n",
" img = mean + (img - mean) * contrast\n",
" img = img + rng.normal(0, noise, img.shape)\n",
" if invert: # ์ผ๊ฐ ์ด์(IR) ๊ทน์ฑ ๋ฐ์ \n",
" img = 255 - img\n",
" return np.clip(img, 0, 255).astype(np.uint8)\n",
"\n",
"\n",
"# ์ดฌ์ ์กฐ๊ฑด ํ๋ฆฌ์
: ๊ธฐ๋ณธ + ๋ณตํฉ ์ํ ์๋๋ฆฌ์ค\n",
"CONDITIONS = {\n",
" \"basic\": {},\n",
" \"offcenter\": {\"shift\": 5.0, \"fore\": (.6, .85)},\n",
" \"far\": {\"scale\": (.32, .45), \"blur\": .6},\n",
" \"blur\": {\"blur\": (1.2, 1.8)},\n",
" \"lowcon\": {\"contrast\": (.2, .35)},\n",
" \"clutter\": {\"clutter\": .8},\n",
" \"occlude\": {\"occlude\": .08},\n",
" \"oblique\": {\"fore\": (.35, .6)},\n",
" \"night\": {\"invert\": True, \"noise\": 10.0},\n",
" \"noise\": {\"noise\": 28.0},\n",
" \"combo\": {\"scale\": (.4, .55), \"blur\": (.8, 1.1), \"noise\": 12.0, \"contrast\": (.6, .8)},\n",
"}\n",
"\n",
"\n",
"def draw_cond(cls, cond, rng):\n",
" kw = {}\n",
" for k, v in CONDITIONS[cond].items():\n",
" kw[k] = rng.uniform(*v) if isinstance(v, tuple) else v\n",
" return draw(cls, rng, **kw)\n",
"\n",
"\n",
"def make_dataset(n_per_class, seed, conds=(\"basic\",)):\n",
" \"\"\"(์ด๋ฏธ์ง ๋ฐฐ์ด N x 32 x 32, ์ ๋ต ๋ผ๋ฒจ, ์กฐ๊ฑด) ๋ฐํ. ์กฐ๊ฑด์ conds์์ ๋ฒ๊ฐ์ ๊ณ ๋ฅธ๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" X, y, c = [], [], []\n",
" for cls in CLASSES:\n",
" for i in range(n_per_class):\n",
" cond = conds[i % len(conds)]\n",
" X.append(draw_cond(cls, cond, rng)); y.append(cls); c.append(cond)\n",
" return np.array(X), np.array(y), np.array(c)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์ ์ฒ๋ฆฌ(๋ฐฐ์น ๋ฒกํฐํ)ยทํํ ํน์ง ๊ณตํต ํจ์ ----\n",
"OUT = 24 # ํฌ๊ธฐ ์ ๊ทํ ๊ฒฐ๊ณผ(24x24)\n",
"NOISE_T = 12.0 # ์ก์ ์ถ์ ๊ฐ์ด ์ด๋ณด๋ค ํฌ๋ฉด 3x3 ์ค๊ฐ๊ฐ ํํฐ ์ ์ฉ(์
์ฒํยท์ผ์ ์ก์)\n",
"FEATS = [\"elong\", \"fill\", \"solidity\", \"n_parts\", \"end_gap\", \"mid_bulge\", \"w_max\", \"compact\"]\n",
"\n",
"\n",
"def preprocess(images, noise_t=NOISE_T):\n",
" \"\"\"N์ฅ ์ผ๊ด ์ฒ๋ฆฌ: ๊ฒฐ์ธก ๋ณด์ -> ๊ทน์ฑ(์ผ๊ฐ ๋ฐ์ ) ๋ณด์ -> ์กฐ๊ฑด๋ณ ์ก์ ์ ๊ฑฐ -> ๋ช
์ ๋๋น ๊ฐํ -> ๋ถํ -> ์ค๊ณฝ์ -> ๋ฐฉํฅยทํฌ๊ธฐ ์ ๊ทํ.\"\"\"\n",
" f = np.array(images, dtype=float).reshape(-1, IMG, IMG)\n",
" n = len(f)\n",
" bad = ~np.isfinite(f)\n",
" missing = bad.sum(axis=(1, 2))\n",
" if bad.any(): # ๊ฒฐ์ธก ํฝ์
: ์์ ์ค์๊ฐ์ผ๋ก ์ฑ์ด ๋ค ์ฃผ๋ณ 3x3 ์ค๊ฐ๊ฐ์ผ๋ก ๋ณด์ \n",
" med = np.array([np.median(x[np.isfinite(x)]) if np.isfinite(x).any() else 128.0 for x in f])\n",
" f = np.where(bad, med[:, None, None], f)\n",
" f = np.where(bad, ndimage.median_filter(f, size=(1, 3, 3)), f)\n",
" border = np.concatenate([f[:, 0], f[:, -1], f[:, :, 0], f[:, :, -1]], axis=1)\n",
" inverted = np.median(border, axis=1) < 128 # ๋ฐฐ๊ฒฝ์ด ์ด๋์ฐ๋ฉด ์ผ๊ฐ ์ด์์ผ๋ก ๋ณด๊ณ ๋ฐ์ \n",
" f[inverted] = 255 - f[inverted]\n",
" med3 = ndimage.median_filter(f, size=(1, 3, 3))\n",
" noise = 1.4826 * np.median(np.abs(f - med3).reshape(n, -1), axis=1)\n",
" noisy = noise > noise_t\n",
" f[noisy] = med3[noisy]\n",
" lo, hi = np.percentile(f.reshape(n, -1), [1, 99], axis=1)\n",
" e = np.clip((f - lo[:, None, None]) / np.maximum(hi - lo, 1e-6)[:, None, None], 0, 1) # ๋ช
์ ๋๋น ๊ฐํ\n",
" mask = e < _otsu(e)[:, None, None]\n",
" mask &= ndimage.uniform_filter(mask.astype(float), size=(1, 3, 3)) * 9 >= 2.5 # ๊ณ ๋ฆฝ ์ ์ก์ ์ ๊ฑฐ\n",
" edge = np.hypot(ndimage.sobel(e, axis=1), ndimage.sobel(e, axis=2)) # ์ค๊ณฝ์ (Sobel)\n",
" ok = (mask.sum(axis=(1, 2)) >= 4) & (hi - lo > 8)\n",
" return {\"enh\": e, \"mask\": mask, \"edge\": edge / np.maximum(edge.max(axis=(1, 2)), 1e-6)[:, None, None],\n",
" \"norm\": _normalize(1 - e, mask, ok), \"inverted\": inverted, \"noise\": noise, \"denoised\": noisy,\n",
" \"missing\": missing, \"contrast\": hi - lo, \"ok\": ok}\n",
"\n",
"\n",
"def _otsu(e, bins=64):\n",
" n = len(e)\n",
" idx = np.minimum((e * bins).astype(int), bins - 1) + bins * np.arange(n)[:, None, None]\n",
" hist = np.bincount(idx.ravel(), minlength=bins * n).reshape(n, bins).astype(float)\n",
" centers = (np.arange(bins) + 0.5) / bins\n",
" w = np.cumsum(hist, axis=1); m = np.cumsum(hist * centers, axis=1)\n",
" tot, mt = w[:, -1:], m[:, -1:]\n",
" between = (mt * w / tot - m) ** 2 / np.maximum(w * (tot - w), 1)\n",
" return (np.argmax(between, axis=1) + 1) / bins\n",
"\n",
"\n",
"def _normalize(obj, mask, ok):\n",
" \"\"\"์ฃผ์ถ ๋ฐฉํฅ์ ์ธ๋ก๋ก ๋๋ฆฌ๊ณ ๋นํ์ฒด ์์ญ๋ง 24x24๋ก ๋ค์ ์ํ๋งํ๋ค(ํ์ ยทํฌ๊ธฐยท์์น ๋ถ๋ณ). ํ ๋ฒ์ ๋ณด๊ฐ์ผ๋ก ์ผ๊ด ์ฒ๋ฆฌ.\"\"\"\n",
" n = len(obj)\n",
" w = mask.astype(float)\n",
" m00 = np.maximum(w.sum(axis=(1, 2)), 1)\n",
" yy, xx = np.mgrid[0:IMG, 0:IMG].astype(float)\n",
" cy, cx = (w * yy).sum(axis=(1, 2)) / m00, (w * xx).sum(axis=(1, 2)) / m00\n",
" dy, dx = yy - cy[:, None, None], xx - cx[:, None, None]\n",
" mu20, mu02, mu11 = (w * dy * dy).sum(axis=(1, 2)), (w * dx * dx).sum(axis=(1, 2)), (w * dy * dx).sum(axis=(1, 2))\n",
" th = 0.5 * np.arctan2(2 * mu11, mu20 - mu02)\n",
" a = np.stack([np.cos(th), np.sin(th)], 1) # ์ฃผ์ถ(y, x)\n",
" b = np.stack([-np.sin(th), np.cos(th)], 1) # ๋ถ์ถ\n",
" pa = np.where(mask, dy * a[:, 0, None, None] + dx * a[:, 1, None, None], 0)\n",
" pb = np.where(mask, dy * b[:, 0, None, None] + dx * b[:, 1, None, None], 0)\n",
" half = np.maximum(np.abs(pa).max(axis=(1, 2)), np.abs(pb).max(axis=(1, 2))) + 1.0\n",
" g = np.linspace(-1, 1, OUT)\n",
" U, V = np.meshgrid(g, g, indexing=\"ij\")\n",
" Y = cy[:, None, None] + half[:, None, None] * (U * a[:, 0, None, None] + V * b[:, 0, None, None])\n",
" X = cx[:, None, None] + half[:, None, None] * (U * a[:, 1, None, None] + V * b[:, 1, None, None])\n",
" N = np.broadcast_to(np.arange(n)[:, None, None], Y.shape)\n",
" out = ndimage.map_coordinates(obj * mask, [N, Y, X], order=1, cval=0.0)\n",
" skew = (out * (U ** 3)).sum(axis=(1, 2))\n",
" out[skew < 0] = out[skew < 0, ::-1] # ๋ฌด๊ฒ๊ฐ ์ ๋ฆฐ ์ชฝ์ ์๋ก(180๋ ๋ชจํธ์ฑ ์ ๊ฑฐ)\n",
" out[~ok] = 0\n",
" return np.clip(out, 0, 1)\n",
"\n",
"\n",
"def shape_features(mask):\n",
" \"\"\"๋ถํ ๋ง์คํฌ 1์ฅ์์ ํ์ ์ ๋ฌด๊ดํ ํํ ํน์ง 8์ข
์ ๊ณ์ฐํ๋ค.\"\"\"\n",
" if mask.sum() < 4:\n",
" return dict.fromkeys(FEATS, 0.0)\n",
" yx = np.argwhere(mask).astype(float)\n",
" c = yx - yx.mean(axis=0)\n",
" val, vec = np.linalg.eigh(np.cov(c.T) + 1e-6 * np.eye(2))\n",
" t, s = c @ vec[:, 1], c @ vec[:, 0]\n",
" length, width = np.ptp(t) + 1, np.ptp(s) + 1\n",
" corners = np.vstack([yx + d for d in ([.5, .5], [.5, -.5], [-.5, .5], [-.5, -.5])])\n",
" try:\n",
" hull = ConvexHull(corners).volume\n",
" except Exception:\n",
" hull = float(mask.sum())\n",
" bins = np.digitize(t, np.linspace(t.min(), t.max() + 1e-6, 4)[1:-1])\n",
" w = np.array([np.ptp(s[bins == k]) + 1 if (bins == k).any() else 0 for k in range(3)]) / length\n",
" boundary = mask & ~ndimage.binary_erosion(mask)\n",
" return {\"elong\": float(np.sqrt(val[1] / max(val[0], 1e-6))), \"fill\": float(mask.sum() / (length * width)),\n",
" \"solidity\": float(mask.sum() / max(hull, 1)), \"n_parts\": float(ndimage.label(mask)[1]),\n",
" \"end_gap\": float(abs(w[0] - w[2])), \"mid_bulge\": float(w[1] - max(w[0], w[2])),\n",
" \"w_max\": float(w.max()), \"compact\": float(boundary.sum() / np.sqrt(mask.sum()))}\n",
"\n",
"\n",
"def feature_matrix(images):\n",
" \"\"\"์ ์ฒ๋ฆฌ ๊ฒฐ๊ณผ, ํํ ํน์ง ํ๋ ฌ(N x 8), ์ ๊ทํ ์ด๋ฏธ์ง ํ๋ ฌ(N x 576)์ ๋๋ ค์ค๋ค.\"\"\"\n",
" P = preprocess(images)\n",
" F = np.array([[d[k] for k in FEATS] for d in map(shape_features, P[\"mask\"])])\n",
" return P, F, P[\"norm\"].reshape(len(F), -1)\n",
"\n",
"# ---- ๋ถ๋ฅ ๋ชจ๋ธ ๊ณตํต ํจ์(๋ค์ธต ํผ์
ํธ๋ก : ์ ๊ทํ ์ด๋ฏธ์ง 576ํฝ์
+ ํํ ํน์ง 8์ข
) ----\n",
"def to_input(F, N, scaler):\n",
" return np.c_[N, scaler.transform(F) * 0.3]\n",
"\n",
"\n",
"def train_model(n_per_class=150, seed=5, conds=tuple(CONDITIONS), hidden=(64,)):\n",
" X, y, _ = make_dataset(n_per_class, seed, conds)\n",
" P, F, N = feature_matrix(X)\n",
" scaler = StandardScaler().fit(F)\n",
" model = MLPClassifier(hidden, max_iter=200, early_stopping=True, random_state=0)\n",
" model.fit(to_input(F, N, scaler), [CLASSES.index(v) for v in y])\n",
" return model, scaler\n",
"\n",
"\n",
"def predict(model, scaler, images):\n",
" \"\"\"(์ ์ฒ๋ฆฌ ๊ฒฐ๊ณผ, ํด๋์ค๋ณ ํ๋ฅ N x 6)์ ๋๋ ค์ค๋ค.\"\"\"\n",
" P, F, N = feature_matrix(images)\n",
" return P, model.predict_proba(to_input(F, N, scaler))\n",
"\n",
"WEIGHTS = {\"p_max\": 0.6, \"margin\": 0.4} # ์ ๋ขฐ๋ ๊ฐ์ค์น ๋งคํธ๋ฆญ์ค(ํ๋ฅ ๋ ํ๋ฅ ์ฐจ์ด)\n",
"HIGH, MEDIUM = 0.8, 0.5\n",
"\n",
"\n",
"def quality(P):\n",
" \"\"\"์์ ํ์ง 0.5~1: ๋๋น๊ฐ ๋ฎ๊ฑฐ๋, ํ์ ์ด ์๊ฑฐ๋, ์ก์์ด ํฌ๋ฉด ๋ฎ์์ง๋ค.\"\"\"\n",
" size = P[\"mask\"].sum(axis=(1, 2))\n",
" q = np.clip(P[\"contrast\"] / 120, 0, 1) * 0.4 + np.clip(size / 60, 0, 1) * 0.4 + np.clip(1 - P[\"noise\"] / 40, 0, 1) * 0.2\n",
" return 0.5 + 0.5 * q\n",
"\n",
"\n",
"def confidence(P, prob):\n",
" s = np.sort(prob, axis=1)\n",
" score = (WEIGHTS[\"p_max\"] * s[:, -1] + WEIGHTS[\"margin\"] * (s[:, -1] - s[:, -2])) * quality(P)\n",
" grade = np.where(score >= HIGH, \"High\", np.where(score >= MEDIUM, \"Medium\", \"Low\"))\n",
" return score, grade\n",
"\n",
"\n",
"def report(name, alert, y):\n",
" true_thr = np.isin(y, list(THREAT))\n",
" far = (alert & ~true_thr).sum() / max(alert.sum(), 1)\n",
" return {\"๋ฐฉ์\": name, \"์ํ ๊ฒฝ๋ณด\": int(alert.sum()), \"์ค๊ฒฝ๋ณด\": int((alert & ~true_thr).sum()), \"์ค๊ฒฝ๋ณด์จ\": round(far, 3),\n",
" \"์ํ ์ฌํ์จ\": round((alert & true_thr).sum() / true_thr.sum(), 3)}\n",
"\n",
"\n",
"model, scaler = train_model(200, seed=5, hidden=(96,))\n",
"X, y, cond = make_dataset(110, 21, tuple(CONDITIONS)) # ๊ฒ์ฆ์ฉ 660์ฅ(ํ์ต์ ์ฐ์ง ์์)\n",
"P, prob = predict(model, scaler, X)\n",
"pred = np.array(CLASSES)[prob.argmax(1)]\n",
"score, grade = confidence(P, prob)\n",
"is_thr_pred = np.isin(pred, list(THREAT))\n",
"\n",
"before = report(\"๊ธฐ์กด(์ต๊ณ ํ๋ฅ ํด๋์ค๋ก ๋จ์ )\", is_thr_pred, y)\n",
"after = report(\"์ ๋ขฐ๋ ์์ง(Low๋ ๊ต๊ด ํ์ธ)\", is_thr_pred & (grade != \"Low\"), y)\n",
"print(pd.DataFrame([before, 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 '๋ฏธ๋ฌ'}), ๊ต๊ด ํ์ธ์ผ๋ก ๋๊ธด ์ํ ํ์ {int((is_thr_pred & (grade == 'Low')).sum())}๊ฑด\")\n",
"\n",
"# ์ผ๊ด์ฑ: ๊ฐ์ ์ด๋ฏธ์ง์ ๋ฏธ์ธ ์ก์(๋ฐ๊ธฐ ยฑ1)์ ๋ํด ๋ค์ ๊ณ์ฐํ์ ๋ ๊ฐ์ ๋ฑ๊ธ์ด ๋์ค๋ ๋น์จ\n",
"rng = np.random.default_rng(0)\n",
"P2, prob2 = predict(model, scaler, np.clip(X + rng.integers(-1, 2, X.shape), 0, 255))\n",
"cons = (confidence(P2, prob2)[1] == grade).mean()\n",
"print(f\"์ ๋ขฐ๋ ์ ์ ์ฐ์ถ ์ผ๊ด์ฑ {cons:.1%} (๋ชฉํ 90% ์ด์: {'๋ฌ์ฑ' if cons >= 0.9 else '๋ฏธ๋ฌ'})\")\n",
"\n",
"p_threat = prob[:, [CLASSES.index(c) for c in THREAT]].sum(1)\n",
"auc = roc_auc_score(np.isin(y, list(THREAT)), p_threat)\n",
"print(f\"์ํ ํ๋ณ ROC-AUC {auc:.3f}\")\n",
"\n",
"print(\"\\n[์๊ณ๊ฐ ๋ฏผ๊ฐ๋: Medium ๊ธฐ์ค๊ฐ๋ณ]\")\n",
"for m in [0.3, 0.4, 0.5, 0.6, 0.7]:\n",
" r = report(f\"Medium>={m}\", is_thr_pred & (score >= m), y)\n",
" print(f\"๊ธฐ์ค {m}: ์ค๊ฒฝ๋ณด์จ {r['์ค๊ฒฝ๋ณด์จ']:.3f}, ์ํ ์ฌํ์จ {r['์ํ ์ฌํ์จ']:.3f}\")\n",
"\n",
"print(\"\\n[๋ฑ๊ธ ๋ถํฌ: ์ค์ ํด๋์ค๋ณ]\")\n",
"print(pd.crosstab(pd.Series(y, name=\"์ค์ \"), pd.Series(grade, name=\"๋ฑ๊ธ\")))\n",
"pair = (y == \"civil_fw\") | (y == \"attack_fw\") | (y == \"civil_quad\")\n",
"print(f\"๋ฏผ๊ฐ ๋๋ก vs ์ํญ ๋๋ก ์ ์ ํ๋ {(pred[pair] == y[pair]).mean():.1%}, ํ๊ท ์ ๋ขฐ๋ {score[pair].mean():.2f}\")\n",
"print(\"๋ฑ๊ธ์ ๊ต์ก์ฉ ํ๋จ ๋ณด์กฐ์ด๋ฉฐ, Low ๋ฑ๊ธ์ ์ ๋ต์ ๋จ์ ํ์ง ์๊ณ ๊ต๊ด ํ์ธ์ผ๋ก ๋๊ธด๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(11, 4))\n",
"fpr, tpr, _ = roc_curve(np.isin(y, list(THREAT)), p_threat)\n",
"ax[0].plot(fpr, tpr); ax[0].plot([0, 1], [0, 1], \"k--\"); ax[0].set_title(f\"Threat ROC (AUC={auc:.3f})\")\n",
"ax[0].set_xlabel(\"false positive rate\"); ax[0].set_ylabel(\"true positive rate\")\n",
"for g, col in [(\"High\", \"tab:green\"), (\"Medium\", \"tab:orange\"), (\"Low\", \"tab:red\")]:\n",
" ax[1].hist(score[grade == g], bins=15, range=(0, 1), color=col, alpha=0.7, label=g)\n",
"ax[1].set_title(\"Confidence score (uncertainty view)\"); ax[1].set_xlabel(\"score\"); ax[1].legend()\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
05 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
" ๋ฐฉ์ ์ํ ๊ฒฝ๋ณด ์ค๊ฒฝ๋ณด ์ค๊ฒฝ๋ณด์จ ์ํ ์ฌํ์จ\n",
" ๊ธฐ์กด(์ต๊ณ ํ๋ฅ ํด๋์ค๋ก ๋จ์ ) 334 29 0.087 0.924\n",
"์ ๋ขฐ๋ ์์ง(Low๋ ๊ต๊ด ํ์ธ) 281 5 0.018 0.836\n",
"\n",
"์ค๊ฒฝ๋ณด์จ ๊ฐ์ถ 79.3% (๋ชฉํ 40% ์ด์: ๋ฌ์ฑ), ๊ต๊ด ํ์ธ์ผ๋ก ๋๊ธด ์ํ ํ์ 53๊ฑด\n",
"์ ๋ขฐ๋ ์ ์ ์ฐ์ถ ์ผ๊ด์ฑ 97.3% (๋ชฉํ 90% ์ด์: ๋ฌ์ฑ)\n",
"์ํ ํ๋ณ ROC-AUC 0.983\n",
"\n",
"[์๊ณ๊ฐ ๋ฏผ๊ฐ๋: Medium ๊ธฐ์ค๊ฐ๋ณ]\n",
"๊ธฐ์ค 0.3: ์ค๊ฒฝ๋ณด์จ 0.054, ์ํ ์ฌํ์จ 0.894\n",
"๊ธฐ์ค 0.4: ์ค๊ฒฝ๋ณด์จ 0.024, ์ํ ์ฌํ์จ 0.867\n",
"๊ธฐ์ค 0.5: ์ค๊ฒฝ๋ณด์จ 0.018, ์ํ ์ฌํ์จ 0.836\n",
"๊ธฐ์ค 0.6: ์ค๊ฒฝ๋ณด์จ 0.011, ์ํ ์ฌํ์จ 0.809\n",
"๊ธฐ์ค 0.7: ์ค๊ฒฝ๋ณด์จ 0.012, ์ํ ์ฌํ์จ 0.779\n",
"\n",
"[๋ฑ๊ธ ๋ถํฌ: ์ค์ ํด๋์ค๋ณ]\n",
"๋ฑ๊ธ High Low Medium\n",
"์ค์ \n",
"attack_fw 99 5 6\n",
"ballistic 72 18 20\n",
"bird 48 29 33\n",
"civil_fw 73 18 19\n",
"civil_quad 90 13 7\n",
"cruise 66 27 17\n",
"๋ฏผ๊ฐ ๋๋ก vs ์ํญ ๋๋ก ์ ์ ํ๋ 91.2%, ํ๊ท ์ ๋ขฐ๋ 0.86\n",
"๋ฑ๊ธ์ ๊ต์ก์ฉ ํ๋จ ๋ณด์กฐ์ด๋ฉฐ, Low ๋ฑ๊ธ์ ์ ๋ต์ ๋จ์ ํ์ง ์๊ณ ๊ต๊ด ํ์ธ์ผ๋ก ๋๊ธด๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
06. ์ฅ๋ณ ๋์ ๋ชจ๋ฐ์ผ/์น ๋ํํ ์ค๋ฃจ์ฃ ์๋ณ ํ์ต ์ฑ UI ํ๋กํ ํ์
๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ฅ๋ณ ๋์ ๋ชจ๋ฐ์ผ/์น ๋ํํ ์ค๋ฃจ์ฃ ์๋ณ ํ์ต ์ฑ UI ํ๋กํ ํ์
์ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ฅ๋ณ์ด ์ค๋ฃจ์ฃ์ ๋ณด๊ณ ๋นํ์ฒด๋ฅผ ๊ณ ๋ฅด๋ ๋ชจ๋ฐ์ผยท์น ๋ํํ ์๋ณ ํ์ต ์ฑ UI ํ๋กํ ํ์
์ ๊ตฌํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๋ฌธํญ ์ถ์ (์ ๋ต๊ณผ ํผ๋ํ๊ธฐ ์ฌ์ด ๋ณด๊ธฐ ํฌํจ), ์๋ต ์ฑ์ , ์ฆ์ ํผ๋๋ฐฑ, ์๋ต ํ AI ์ฐธ๊ณ ํ์ ํ์, ์งํ๋ฅ ยท์ ์ ํ์ ๊ธฐ๋ฅ์ ๊ตฌํํ๊ณ ๊ฐ์ ํ์ต์ ์๋ต์ผ๋ก 12๋ฌธํญ์ ๋ชจ์ ์คํ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/ui/trn03_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": [
"\"\"\"์ฅ๋ณ์ด ์ค๋ฃจ์ฃ์ ๋ณด๊ณ ๋นํ์ฒด๋ฅผ ๊ณ ๋ฅด๋ ๋ํํ ์๋ณ ํ์ต ์ฑ UI ํ๋กํ ํ์
์ ๊ตฌํํ๋ค. ๋ฌธ์ ์ถ์ ยท์๋ต ์ฒ๋ฆฌยท์ฆ์ ํผ๋๋ฐฑยทAI ์ฐธ๊ณ ํ์ ยท์งํ๋ฅ ํ์๋ฅผ ๋ชจ์ ์คํํด ํ๋ฉด ๊ฐฑ์ ์ฃผ๊ธฐ์ ์๋ต ์ฒ๋ฆฌ์๊ฐ์ ์ธก์ ํ๊ณ , ์ผ๊ฐ ๋ชจ๋ ํ์ต ํ๋ฉด์ ๊ทธ๋ฆฐ๋ค.\"\"\"\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from matplotlib.path import Path as MplPath\n",
"from scipy import ndimage\n",
"from scipy.spatial import ConvexHull\n",
"from sklearn.neural_network import MLPClassifier\n",
"from sklearn.preprocessing import StandardScaler\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ํฉ์ฑ ์ค๋ฃจ์ฃ ์์ฑ๊ธฐ (์ค์ ๊ธฐ์ฒด ์ฌ์งยท๋๋ฉดยท์ ์ ์ฌ์ฉ ์ ํจ)\n",
"# ์ฌ๊ฐํยท์ผ๊ฐํยท์ ๊ฐ์ ๊ธฐ๋ณธ ๋ํ์ ์กฐํฉํ ์ถ์ ์ค๋ฃจ์ฃ์ 32x32 ํ๋ฐฑ ์ด๋ฏธ์ง๋ก ๊ทธ๋ฆฐ๋ค.\n",
"# ํ์ (๋ค๊ฐ๋), ์๊ทผ ์ถ์(์ธก๋ฉด ์์ ), ํฌ๊ธฐ(์๊ฑฐ๋ฆฌ), ํ๋ฆผยท์ก์ยท์ ๋๋นยท๊ตฌ๋ฆยท๊ฐ๋ฆผยท์ผ๊ฐ ๋ฐ์ ์ ์กฐ์ ํ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"IMG = 32\n",
"CLASSES = [\"civil_quad\", \"civil_fw\", \"attack_fw\", \"cruise\", \"ballistic\", \"bird\"]\n",
"KOR = {\"civil_quad\": \"๋ฏผ๊ฐ ์ฟผ๋์ฝฅํฐํ\", \"civil_fw\": \"๋ฏผ๊ฐ ๊ณ ์ ์ตํ\", \"attack_fw\": \"์ผ๊ฐ์ต ์ํญ๋๋ก ํ\",\n",
" \"cruise\": \"์ํญ๋ฏธ์ฌ์ผํ\", \"ballistic\": \"ํ๋๋ฏธ์ฌ์ผํ\", \"bird\": \"์กฐ๋ฅ\"}\n",
"THREAT = {\"attack_fw\", \"cruise\", \"ballistic\"}\n",
"\n",
"\n",
"def _rect(x0, x1, y0, y1):\n",
" return [(x0, y0), (x1, y0), (x1, y1), (x0, y1)]\n",
"\n",
"\n",
"def _bar(p, q, w):\n",
" (x0, y0), (x1, y1) = p, q\n",
" n = np.array([y0 - y1, x1 - x0]) / np.hypot(x1 - x0, y1 - y0) * w / 2\n",
" return [(x0 + n[0], y0 + n[1]), (x1 + n[0], y1 + n[1]), (x1 - n[0], y1 - n[1]), (x0 - n[0], y0 - n[1])]\n",
"\n",
"\n",
"# ๋ํ ์ ์(๋จ์ ์ขํ, ๊ธฐ์ ๋ฐฉํฅ = +y). ๋ชจ๋ ์ถ์ ๋ํ์ด๋ฉฐ ํน์ ๊ธฐ์ข
์ ๋ณธ๋จ์ง ์๋๋ค.\n",
"SHAPES = {\n",
" \"civil_quad\": {\"poly\": [_bar((-.55, -.55), (.55, .55), .1), _bar((-.55, .55), (.55, -.55), .1), _rect(-.15, .15, -.15, .15)],\n",
" \"circ\": [(-.55, -.55, .24), (.55, .55, .24), (-.55, .55, .24), (.55, -.55, .24)]},\n",
" \"civil_fw\": {\"poly\": [_rect(-.07, .07, -.8, .72), _rect(-.9, .9, .05, .25), _rect(-.33, .33, -.8, -.66)], \"circ\": []},\n",
" \"attack_fw\": {\"poly\": [[(0, .85), (.85, -.5), (.12, -.42), (0, -.6), (-.12, -.42), (-.85, -.5)]], \"circ\": []},\n",
" \"cruise\": {\"poly\": [_rect(-.07, .07, -.9, .7), [(-.07, .7), (0, .92), (.07, .7)], _rect(-.42, .42, -.05, .07),\n",
" [(-.07, -.9), (-.24, -.95), (-.07, -.74)], [(.07, -.9), (.24, -.95), (.07, -.74)]], \"circ\": []},\n",
" \"ballistic\": {\"poly\": [_rect(-.13, .13, -.85, .6), [(-.13, .6), (0, .95), (.13, .6)],\n",
" [(-.13, -.85), (-.34, -.95), (-.13, -.58)], [(.13, -.85), (.34, -.95), (.13, -.58)]], \"circ\": []},\n",
" \"bird\": {\"poly\": [[(-.9, .12), (-.5, .28), (-.12, .06), (0, .22), (.12, .06), (.5, .28), (.9, .12), (.5, .12),\n",
" (.12, -.1), (0, -.38), (-.12, -.1), (-.5, .12)]], \"circ\": []},\n",
"}\n",
"_S = 2\n",
"_g = (np.arange(IMG * _S) + 0.5) / _S\n",
"_GX, _GY = np.meshgrid(_g, _g)\n",
"_PTS = np.c_[_GX.ravel(), _GY.ravel()]\n",
"_PATHS = {c: [MplPath(np.array(p)) for p in s[\"poly\"]] for c, s in SHAPES.items()}\n",
"\n",
"\n",
"def draw(cls, rng, angle=None, scale=None, fore=None, blur=0.3, noise=4.0, contrast=1.0,\n",
" clutter=0.0, occlude=0.0, invert=False, shift=1.5):\n",
" \"\"\"ํด๋์ค 1๊ฐ ์ค๋ฃจ์ฃ ์ด๋ฏธ์ง(uint8 32x32)๋ฅผ ๊ทธ๋ฆฐ๋ค. ๋ฐฐ๊ฒฝ์ ๋ฐ์ ํ๋, ๋นํ์ฒด๋ ์ด๋์ด ๊ทธ๋ฆผ์.\"\"\"\n",
" angle = rng.uniform(0, 360) if angle is None else angle\n",
" scale = rng.uniform(.7, .95) if scale is None else scale\n",
" fore = rng.uniform(.85, 1.0) if fore is None else fore\n",
" th = np.radians(angle)\n",
" c0 = IMG / 2 + rng.uniform(-shift, shift, 2)\n",
" d = (_PTS - c0) / (scale * IMG / 2) # ์ด๋ฏธ์ง ์ขํ -> ๋ํ ์ขํ(์ญ๋ณํ)\n",
" u = d[:, 0] * np.cos(th) + d[:, 1] * np.sin(th)\n",
" v = (-d[:, 0] * np.sin(th) + d[:, 1] * np.cos(th)) / fore # ์๊ทผ ์ถ์(๊ธฐ์ ๋ฐฉํฅ์ผ๋ก ์งง์ ๋ณด์)\n",
" uv = np.c_[u, -v]\n",
" m = np.zeros(len(uv), bool)\n",
" near = np.flatnonzero(np.abs(uv).max(axis=1) <= 1.0) # ๋ํ์ด ์์ ์ ์๋ ์์ญ๋ง ๊ฒ์ฌ(์๋)\n",
" for p in _PATHS[cls]:\n",
" m[near] |= p.contains_points(uv[near])\n",
" for cx, cy, r in SHAPES[cls][\"circ\"]:\n",
" m |= (uv[:, 0] - cx) ** 2 + (uv[:, 1] - cy) ** 2 <= r * r\n",
" cov = m.reshape(IMG, _S, IMG, _S).mean(axis=(1, 3))\n",
" if occlude > 0: # ๋ถ๋ถ ๊ฐ๋ฆผ(๊ตฌ๋ฆ ์กฐ๊ฐ)\n",
" h = int(IMG * np.sqrt(occlude)); y0, x0 = rng.integers(8, IMG - 8 - h // 2, 2)\n",
" cov[y0:y0 + h, x0:x0 + h] = 0\n",
" bg = rng.uniform(180, 220) + np.linspace(-10, 10, IMG)[:, None]\n",
" fg = rng.uniform(30, 60)\n",
" img = bg * (1 - cov) + fg * cov\n",
" if clutter > 0: # ๊ตฌ๋ฆยท๋ฐฐ๊ฒฝ ์ผ๋ฃฉ\n",
" img -= clutter * 90 * np.clip(ndimage.gaussian_filter(rng.normal(0, 1, (IMG, IMG)), 3) * 4, 0, None)\n",
" if blur > 0:\n",
" img = ndimage.gaussian_filter(img, blur)\n",
" mean = img.mean()\n",
" img = mean + (img - mean) * contrast\n",
" img = img + rng.normal(0, noise, img.shape)\n",
" if invert: # ์ผ๊ฐ ์ด์(IR) ๊ทน์ฑ ๋ฐ์ \n",
" img = 255 - img\n",
" return np.clip(img, 0, 255).astype(np.uint8)\n",
"\n",
"\n",
"# ์ดฌ์ ์กฐ๊ฑด ํ๋ฆฌ์
: ๊ธฐ๋ณธ + ๋ณตํฉ ์ํ ์๋๋ฆฌ์ค\n",
"CONDITIONS = {\n",
" \"basic\": {},\n",
" \"offcenter\": {\"shift\": 5.0, \"fore\": (.6, .85)},\n",
" \"far\": {\"scale\": (.32, .45), \"blur\": .6},\n",
" \"blur\": {\"blur\": (1.2, 1.8)},\n",
" \"lowcon\": {\"contrast\": (.2, .35)},\n",
" \"clutter\": {\"clutter\": .8},\n",
" \"occlude\": {\"occlude\": .08},\n",
" \"oblique\": {\"fore\": (.35, .6)},\n",
" \"night\": {\"invert\": True, \"noise\": 10.0},\n",
" \"noise\": {\"noise\": 28.0},\n",
" \"combo\": {\"scale\": (.4, .55), \"blur\": (.8, 1.1), \"noise\": 12.0, \"contrast\": (.6, .8)},\n",
"}\n",
"\n",
"\n",
"def draw_cond(cls, cond, rng):\n",
" kw = {}\n",
" for k, v in CONDITIONS[cond].items():\n",
" kw[k] = rng.uniform(*v) if isinstance(v, tuple) else v\n",
" return draw(cls, rng, **kw)\n",
"\n",
"\n",
"def make_dataset(n_per_class, seed, conds=(\"basic\",)):\n",
" \"\"\"(์ด๋ฏธ์ง ๋ฐฐ์ด N x 32 x 32, ์ ๋ต ๋ผ๋ฒจ, ์กฐ๊ฑด) ๋ฐํ. ์กฐ๊ฑด์ conds์์ ๋ฒ๊ฐ์ ๊ณ ๋ฅธ๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" X, y, c = [], [], []\n",
" for cls in CLASSES:\n",
" for i in range(n_per_class):\n",
" cond = conds[i % len(conds)]\n",
" X.append(draw_cond(cls, cond, rng)); y.append(cls); c.append(cond)\n",
" return np.array(X), np.array(y), np.array(c)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์ ์ฒ๋ฆฌ(๋ฐฐ์น ๋ฒกํฐํ)ยทํํ ํน์ง ๊ณตํต ํจ์ ----\n",
"OUT = 24 # ํฌ๊ธฐ ์ ๊ทํ ๊ฒฐ๊ณผ(24x24)\n",
"NOISE_T = 12.0 # ์ก์ ์ถ์ ๊ฐ์ด ์ด๋ณด๋ค ํฌ๋ฉด 3x3 ์ค๊ฐ๊ฐ ํํฐ ์ ์ฉ(์
์ฒํยท์ผ์ ์ก์)\n",
"FEATS = [\"elong\", \"fill\", \"solidity\", \"n_parts\", \"end_gap\", \"mid_bulge\", \"w_max\", \"compact\"]\n",
"\n",
"\n",
"def preprocess(images, noise_t=NOISE_T):\n",
" \"\"\"N์ฅ ์ผ๊ด ์ฒ๋ฆฌ: ๊ฒฐ์ธก ๋ณด์ -> ๊ทน์ฑ(์ผ๊ฐ ๋ฐ์ ) ๋ณด์ -> ์กฐ๊ฑด๋ณ ์ก์ ์ ๊ฑฐ -> ๋ช
์ ๋๋น ๊ฐํ -> ๋ถํ -> ์ค๊ณฝ์ -> ๋ฐฉํฅยทํฌ๊ธฐ ์ ๊ทํ.\"\"\"\n",
" f = np.array(images, dtype=float).reshape(-1, IMG, IMG)\n",
" n = len(f)\n",
" bad = ~np.isfinite(f)\n",
" missing = bad.sum(axis=(1, 2))\n",
" if bad.any(): # ๊ฒฐ์ธก ํฝ์
: ์์ ์ค์๊ฐ์ผ๋ก ์ฑ์ด ๋ค ์ฃผ๋ณ 3x3 ์ค๊ฐ๊ฐ์ผ๋ก ๋ณด์ \n",
" med = np.array([np.median(x[np.isfinite(x)]) if np.isfinite(x).any() else 128.0 for x in f])\n",
" f = np.where(bad, med[:, None, None], f)\n",
" f = np.where(bad, ndimage.median_filter(f, size=(1, 3, 3)), f)\n",
" border = np.concatenate([f[:, 0], f[:, -1], f[:, :, 0], f[:, :, -1]], axis=1)\n",
" inverted = np.median(border, axis=1) < 128 # ๋ฐฐ๊ฒฝ์ด ์ด๋์ฐ๋ฉด ์ผ๊ฐ ์ด์์ผ๋ก ๋ณด๊ณ ๋ฐ์ \n",
" f[inverted] = 255 - f[inverted]\n",
" med3 = ndimage.median_filter(f, size=(1, 3, 3))\n",
" noise = 1.4826 * np.median(np.abs(f - med3).reshape(n, -1), axis=1)\n",
" noisy = noise > noise_t\n",
" f[noisy] = med3[noisy]\n",
" lo, hi = np.percentile(f.reshape(n, -1), [1, 99], axis=1)\n",
" e = np.clip((f - lo[:, None, None]) / np.maximum(hi - lo, 1e-6)[:, None, None], 0, 1) # ๋ช
์ ๋๋น ๊ฐํ\n",
" mask = e < _otsu(e)[:, None, None]\n",
" mask &= ndimage.uniform_filter(mask.astype(float), size=(1, 3, 3)) * 9 >= 2.5 # ๊ณ ๋ฆฝ ์ ์ก์ ์ ๊ฑฐ\n",
" edge = np.hypot(ndimage.sobel(e, axis=1), ndimage.sobel(e, axis=2)) # ์ค๊ณฝ์ (Sobel)\n",
" ok = (mask.sum(axis=(1, 2)) >= 4) & (hi - lo > 8)\n",
" return {\"enh\": e, \"mask\": mask, \"edge\": edge / np.maximum(edge.max(axis=(1, 2)), 1e-6)[:, None, None],\n",
" \"norm\": _normalize(1 - e, mask, ok), \"inverted\": inverted, \"noise\": noise, \"denoised\": noisy,\n",
" \"missing\": missing, \"contrast\": hi - lo, \"ok\": ok}\n",
"\n",
"\n",
"def _otsu(e, bins=64):\n",
" n = len(e)\n",
" idx = np.minimum((e * bins).astype(int), bins - 1) + bins * np.arange(n)[:, None, None]\n",
" hist = np.bincount(idx.ravel(), minlength=bins * n).reshape(n, bins).astype(float)\n",
" centers = (np.arange(bins) + 0.5) / bins\n",
" w = np.cumsum(hist, axis=1); m = np.cumsum(hist * centers, axis=1)\n",
" tot, mt = w[:, -1:], m[:, -1:]\n",
" between = (mt * w / tot - m) ** 2 / np.maximum(w * (tot - w), 1)\n",
" return (np.argmax(between, axis=1) + 1) / bins\n",
"\n",
"\n",
"def _normalize(obj, mask, ok):\n",
" \"\"\"์ฃผ์ถ ๋ฐฉํฅ์ ์ธ๋ก๋ก ๋๋ฆฌ๊ณ ๋นํ์ฒด ์์ญ๋ง 24x24๋ก ๋ค์ ์ํ๋งํ๋ค(ํ์ ยทํฌ๊ธฐยท์์น ๋ถ๋ณ). ํ ๋ฒ์ ๋ณด๊ฐ์ผ๋ก ์ผ๊ด ์ฒ๋ฆฌ.\"\"\"\n",
" n = len(obj)\n",
" w = mask.astype(float)\n",
" m00 = np.maximum(w.sum(axis=(1, 2)), 1)\n",
" yy, xx = np.mgrid[0:IMG, 0:IMG].astype(float)\n",
" cy, cx = (w * yy).sum(axis=(1, 2)) / m00, (w * xx).sum(axis=(1, 2)) / m00\n",
" dy, dx = yy - cy[:, None, None], xx - cx[:, None, None]\n",
" mu20, mu02, mu11 = (w * dy * dy).sum(axis=(1, 2)), (w * dx * dx).sum(axis=(1, 2)), (w * dy * dx).sum(axis=(1, 2))\n",
" th = 0.5 * np.arctan2(2 * mu11, mu20 - mu02)\n",
" a = np.stack([np.cos(th), np.sin(th)], 1) # ์ฃผ์ถ(y, x)\n",
" b = np.stack([-np.sin(th), np.cos(th)], 1) # ๋ถ์ถ\n",
" pa = np.where(mask, dy * a[:, 0, None, None] + dx * a[:, 1, None, None], 0)\n",
" pb = np.where(mask, dy * b[:, 0, None, None] + dx * b[:, 1, None, None], 0)\n",
" half = np.maximum(np.abs(pa).max(axis=(1, 2)), np.abs(pb).max(axis=(1, 2))) + 1.0\n",
" g = np.linspace(-1, 1, OUT)\n",
" U, V = np.meshgrid(g, g, indexing=\"ij\")\n",
" Y = cy[:, None, None] + half[:, None, None] * (U * a[:, 0, None, None] + V * b[:, 0, None, None])\n",
" X = cx[:, None, None] + half[:, None, None] * (U * a[:, 1, None, None] + V * b[:, 1, None, None])\n",
" N = np.broadcast_to(np.arange(n)[:, None, None], Y.shape)\n",
" out = ndimage.map_coordinates(obj * mask, [N, Y, X], order=1, cval=0.0)\n",
" skew = (out * (U ** 3)).sum(axis=(1, 2))\n",
" out[skew < 0] = out[skew < 0, ::-1] # ๋ฌด๊ฒ๊ฐ ์ ๋ฆฐ ์ชฝ์ ์๋ก(180๋ ๋ชจํธ์ฑ ์ ๊ฑฐ)\n",
" out[~ok] = 0\n",
" return np.clip(out, 0, 1)\n",
"\n",
"\n",
"def shape_features(mask):\n",
" \"\"\"๋ถํ ๋ง์คํฌ 1์ฅ์์ ํ์ ์ ๋ฌด๊ดํ ํํ ํน์ง 8์ข
์ ๊ณ์ฐํ๋ค.\"\"\"\n",
" if mask.sum() < 4:\n",
" return dict.fromkeys(FEATS, 0.0)\n",
" yx = np.argwhere(mask).astype(float)\n",
" c = yx - yx.mean(axis=0)\n",
" val, vec = np.linalg.eigh(np.cov(c.T) + 1e-6 * np.eye(2))\n",
" t, s = c @ vec[:, 1], c @ vec[:, 0]\n",
" length, width = np.ptp(t) + 1, np.ptp(s) + 1\n",
" corners = np.vstack([yx + d for d in ([.5, .5], [.5, -.5], [-.5, .5], [-.5, -.5])])\n",
" try:\n",
" hull = ConvexHull(corners).volume\n",
" except Exception:\n",
" hull = float(mask.sum())\n",
" bins = np.digitize(t, np.linspace(t.min(), t.max() + 1e-6, 4)[1:-1])\n",
" w = np.array([np.ptp(s[bins == k]) + 1 if (bins == k).any() else 0 for k in range(3)]) / length\n",
" boundary = mask & ~ndimage.binary_erosion(mask)\n",
" return {\"elong\": float(np.sqrt(val[1] / max(val[0], 1e-6))), \"fill\": float(mask.sum() / (length * width)),\n",
" \"solidity\": float(mask.sum() / max(hull, 1)), \"n_parts\": float(ndimage.label(mask)[1]),\n",
" \"end_gap\": float(abs(w[0] - w[2])), \"mid_bulge\": float(w[1] - max(w[0], w[2])),\n",
" \"w_max\": float(w.max()), \"compact\": float(boundary.sum() / np.sqrt(mask.sum()))}\n",
"\n",
"\n",
"def feature_matrix(images):\n",
" \"\"\"์ ์ฒ๋ฆฌ ๊ฒฐ๊ณผ, ํํ ํน์ง ํ๋ ฌ(N x 8), ์ ๊ทํ ์ด๋ฏธ์ง ํ๋ ฌ(N x 576)์ ๋๋ ค์ค๋ค.\"\"\"\n",
" P = preprocess(images)\n",
" F = np.array([[d[k] for k in FEATS] for d in map(shape_features, P[\"mask\"])])\n",
" return P, F, P[\"norm\"].reshape(len(F), -1)\n",
"\n",
"# ---- ๋ถ๋ฅ ๋ชจ๋ธ ๊ณตํต ํจ์(๋ค์ธต ํผ์
ํธ๋ก : ์ ๊ทํ ์ด๋ฏธ์ง 576ํฝ์
+ ํํ ํน์ง 8์ข
) ----\n",
"def to_input(F, N, scaler):\n",
" return np.c_[N, scaler.transform(F) * 0.3]\n",
"\n",
"\n",
"def train_model(n_per_class=150, seed=5, conds=tuple(CONDITIONS), hidden=(64,)):\n",
" X, y, _ = make_dataset(n_per_class, seed, conds)\n",
" P, F, N = feature_matrix(X)\n",
" scaler = StandardScaler().fit(F)\n",
" model = MLPClassifier(hidden, max_iter=200, early_stopping=True, random_state=0)\n",
" model.fit(to_input(F, N, scaler), [CLASSES.index(v) for v in y])\n",
" return model, scaler\n",
"\n",
"\n",
"def predict(model, scaler, images):\n",
" \"\"\"(์ ์ฒ๋ฆฌ ๊ฒฐ๊ณผ, ํด๋์ค๋ณ ํ๋ฅ N x 6)์ ๋๋ ค์ค๋ค.\"\"\"\n",
" P, F, N = feature_matrix(images)\n",
" return P, model.predict_proba(to_input(F, N, scaler))\n",
"\n",
"N_OPTIONS = 4\n",
"CONFUSE = {\"civil_fw\": \"attack_fw\", \"attack_fw\": \"civil_fw\", \"cruise\": \"ballistic\", \"ballistic\": \"cruise\", \"bird\": \"civil_fw\", \"civil_quad\": \"civil_fw\"}\n",
"\n",
"\n",
"class QuizApp:\n",
" \"\"\"ํ์ต ์ฑ ์ํ: ๋ฌธ์ ์ํ, ํ์ฌ ๋ฌธ์ , ์ ์, ์๋ต ์ด๋ ฅ. ํ๋ฉด 1์ฅ = render() ๊ฒฐ๊ณผ ๋ฌธ์์ด.\"\"\"\n",
"\n",
" def __init__(self, model, scaler, rng, n_questions=12):\n",
" self.model, self.scaler, self.rng = model, scaler, rng\n",
" conds = [\"basic\", \"far\", \"oblique\", \"night\", \"blur\", \"clutter\"]\n",
" self.bank = [(c, conds[i % len(conds)]) for i, c in enumerate(rng.permutation(CLASSES * 2))][:n_questions]\n",
" self.i, self.history = 0, []\n",
" self.next_question()\n",
"\n",
" def next_question(self):\n",
" cls, cond = self.bank[self.i]\n",
" self.img, self.answer, self.cond = draw_cond(cls, cond, self.rng), cls, cond\n",
" others = [c for c in CLASSES if c not in (cls, CONFUSE[cls])]\n",
" self.options = list(self.rng.permutation([cls, CONFUSE[cls]] + list(self.rng.choice(others, N_OPTIONS - 2, replace=False))))\n",
" self.feedback = None\n",
"\n",
" def submit(self, choice_no):\n",
" \"\"\"์๋ต ์ฒ๋ฆฌ: ์ฑ์ -> AI ์ฐธ๊ณ ํ์ (์ ๋ขฐ๋) -> ํผ๋๋ฐฑ ๋ฌธ๊ตฌ. ์ฒ๋ฆฌ์๊ฐ(ms)์ ํจ๊ป ๊ธฐ๋กํ๋ค.\"\"\"\n",
" t0 = time.perf_counter()\n",
" choice = self.options[choice_no - 1]\n",
" _, prob = predict(self.model, self.scaler, [self.img])\n",
" ai, conf = CLASSES[int(prob.argmax())], float(prob.max())\n",
" ok = choice == self.answer\n",
" self.feedback = (\"์ ๋ต\" if ok else f\"์ค๋ต - ์ ๋ต์ {KOR[self.answer]}\") + f\" | AI ์ฐธ๊ณ ํ์ : {KOR[ai]}({conf:.0%})\"\n",
" ms = (time.perf_counter() - t0) * 1000\n",
" self.history.append({\"๋ฌธํญ\": self.i + 1, \"์ ๋ต\": self.answer, \"์ ํ\": choice, \"๊ฒฐ๊ณผ\": ok, \"์กฐ๊ฑด\": self.cond, \"์ฒ๋ฆฌms\": ms})\n",
" return ms\n",
"\n",
" def render(self):\n",
" done = len(self.history)\n",
" bar = \"#\" * done + \".\" * (len(self.bank) - done)\n",
" score = sum(h[\"๊ฒฐ๊ณผ\"] for h in self.history)\n",
" opts = \" \".join(f\"{k + 1}) {KOR[o]}\" for k, o in enumerate(self.options))\n",
" return f\"[๋ฌธํญ {self.i + 1}/{len(self.bank)}] ์งํ {bar} ์ ์ {score}/{done}\\n ๋ณด๊ธฐ: {opts}\\n ํผ๋๋ฐฑ: {self.feedback or '-'}\"\n",
"\n",
"\n",
"model, scaler = train_model(120, seed=5)\n",
"rng = np.random.default_rng(8)\n",
"app = QuizApp(model, scaler, rng)\n",
"skill = {\"civil_quad\": .9, \"civil_fw\": .65, \"attack_fw\": .7, \"cruise\": .55, \"ballistic\": .6, \"bird\": .85} # ๊ฐ์ ํ์ต์ ์๋ จ๋\n",
"\n",
"refresh = []\n",
"for q in range(len(app.bank)):\n",
" t_screen = time.perf_counter()\n",
" correct = rng.random() < skill[app.answer]\n",
" pick = app.options.index(app.answer if correct else CONFUSE[app.answer]) + 1\n",
" app.submit(pick)\n",
" if q < 2:\n",
" print(app.render() + \"\\n\")\n",
" refresh.append((time.perf_counter() - t_screen) * 1000)\n",
" if q < len(app.bank) - 1:\n",
" app.i += 1; app.next_question()\n",
"\n",
"h = pd.DataFrame(app.history)\n",
"print(f\"ํ๋ฉด ๊ฐฑ์ ์ฒ๋ฆฌ ์ต๋ {max(refresh):.1f} ms (๋ชฉํ: ๊ฐฑ์ ์ฃผ๊ธฐ 1์ด ์ด๋ด), ์๋ต ์ฒ๋ฆฌ ํ๊ท {h['์ฒ๋ฆฌms'].mean():.1f} ms / ์ต๋ {h['์ฒ๋ฆฌms'].max():.1f} ms (๋ชฉํ 100 ms ์ด๋ด: {'๋ฌ์ฑ' if h['์ฒ๋ฆฌms'].max() <= 100 else '๋ฏธ๋ฌ'})\")\n",
"print(f\"์ต์ข
์ ์ {h['๊ฒฐ๊ณผ'].sum()}/{len(h)}\")\n",
"print(\"\\n[ํด๋์ค๋ณ ์ ๋ต๋ฅ ]\")\n",
"print(h.groupby(\"์ ๋ต\")[\"๊ฒฐ๊ณผ\"].mean().rename(index=KOR).round(2).to_string())\n",
"print(\"\\n[์ค๋ต ๋ชฉ๋ก]\")\n",
"print(h[~h[\"๊ฒฐ๊ณผ\"]][[\"๋ฌธํญ\", \"์ ๋ต\", \"์ ํ\", \"์กฐ๊ฑด\"]].to_string(index=False))\n",
"\n",
"# ํ์ต ํ๋ฉด(์ผ๊ฐ ๋ชจ๋): ๋ฌธ์ ์ด๋ฏธ์ง, ๋ณด๊ธฐยทํผ๋๋ฐฑ, ์งํ๋ฅ , ํด๋์ค๋ณ ์ ๋ต๋ฅ , ์๋ต์๊ฐ ์ถ์ด - ์๊ฐํ ๊ตฌ์ฑ์์ 5์ข
\n",
"plt.style.use(\"dark_background\")\n",
"fig = plt.figure(figsize=(12, 7))\n",
"ax1 = fig.add_subplot(2, 3, 1); ax1.imshow(app.img, cmap=\"gray\"); ax1.axis(\"off\"); ax1.set_title(f\"Q{app.i + 1}: identify the silhouette\")\n",
"ax2 = fig.add_subplot(2, 3, 2); ax2.axis(\"off\"); ax2.set_title(\"Options / feedback\")\n",
"last = app.history[-1]\n",
"for k, o in enumerate(app.options):\n",
" col = \"lime\" if o == app.answer else (\"red\" if o == last[\"์ ํ\"] else \"white\")\n",
" ax2.text(0.05, 0.85 - k * 0.2, f\"{k + 1}) {o}\", color=col, fontsize=13, fontweight=\"bold\" if col != \"white\" else \"normal\")\n",
"ax3 = fig.add_subplot(2, 3, 3); s = h[\"๊ฒฐ๊ณผ\"].sum()\n",
"ax3.barh([0], [len(h)], color=\"dimgray\"); ax3.barh([0], [s], color=\"deepskyblue\"); ax3.set_yticks([])\n",
"ax3.set_title(f\"Progress / score {s}/{len(h)}\")\n",
"ax4 = fig.add_subplot(2, 3, 4); acc = h.groupby(\"์ ๋ต\")[\"๊ฒฐ๊ณผ\"].mean()\n",
"ax4.bar(acc.index, acc.values, color=[\"red\" if v < 0.6 else \"deepskyblue\" for v in acc.values]); ax4.tick_params(axis=\"x\", rotation=40)\n",
"ax4.set_title(\"Accuracy by class (red = weak)\")\n",
"ax5 = fig.add_subplot(2, 3, 5); ax5.plot(h[\"๋ฌธํญ\"], h[\"์ฒ๋ฆฌms\"], \"o-\", color=\"orange\"); ax5.axhline(100, ls=\"--\", c=\"gray\")\n",
"ax5.set_title(\"Response time (ms)\"); ax5.set_xlabel(\"question\")\n",
"ax6 = fig.add_subplot(2, 3, 6); ax6.axis(\"off\")\n",
"ax6.text(0, 0.8, \"Night mode UI\\nCorrect: green / Wrong: red\\nAI hint shown only after answer\", fontsize=11, va=\"top\")\n",
"plt.suptitle(\"Silhouette ID training app prototype - training data only\"); plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
06 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๋ฌธํญ 1/12] ์งํ #........... ์ ์ 1/1\n",
" ๋ณด๊ธฐ: 1) ์กฐ๋ฅ 2) ๋ฏผ๊ฐ ๊ณ ์ ์ตํ 3) ๋ฏผ๊ฐ ์ฟผ๋์ฝฅํฐํ 4) ์ผ๊ฐ์ต ์ํญ๋๋ก ํ\n",
" ํผ๋๋ฐฑ: ์ ๋ต | AI ์ฐธ๊ณ ํ์ : ๋ฏผ๊ฐ ๊ณ ์ ์ตํ(96%)\n",
"\n",
"[๋ฌธํญ 2/12] ์งํ ##.......... ์ ์ 1/2\n",
" ๋ณด๊ธฐ: 1) ํ๋๋ฏธ์ฌ์ผํ 2) ์ผ๊ฐ์ต ์ํญ๋๋ก ํ 3) ์ํญ๋ฏธ์ฌ์ผํ 4) ๋ฏผ๊ฐ ๊ณ ์ ์ตํ\n",
" ํผ๋๋ฐฑ: ์ค๋ต - ์ ๋ต์ ์ํญ๋ฏธ์ฌ์ผํ | AI ์ฐธ๊ณ ํ์ : ์ํญ๋ฏธ์ฌ์ผํ(83%)\n",
"\n",
"ํ๋ฉด ๊ฐฑ์ ์ฒ๋ฆฌ ์ต๋ 8.5 ms (๋ชฉํ: ๊ฐฑ์ ์ฃผ๊ธฐ 1์ด ์ด๋ด), ์๋ต ์ฒ๋ฆฌ ํ๊ท 4.4 ms / ์ต๋ 6.6 ms (๋ชฉํ 100 ms ์ด๋ด: ๋ฌ์ฑ)\n",
"์ต์ข
์ ์ 9/12\n",
"\n",
"[ํด๋์ค๋ณ ์ ๋ต๋ฅ ]\n",
"์ ๋ต\n",
"์ผ๊ฐ์ต ์ํญ๋๋ก ํ 1.0\n",
"ํ๋๋ฏธ์ฌ์ผํ 0.5\n",
"์กฐ๋ฅ 1.0\n",
"๋ฏผ๊ฐ ๊ณ ์ ์ตํ 1.0\n",
"๋ฏผ๊ฐ ์ฟผ๋์ฝฅํฐํ 1.0\n",
"์ํญ๋ฏธ์ฌ์ผํ 0.0\n",
"\n",
"[์ค๋ต ๋ชฉ๋ก]\n",
" ๋ฌธํญ ์ ๋ต ์ ํ ์กฐ๊ฑด\n",
" 2 cruise ballistic far\n",
" 6 ballistic cruise clutter\n",
" 10 cruise ballistic night\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/trn03_approval_workflow.py ๋ฐ docs/trn03_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) ํด์ค ๋ชจ๋์ ๊ตฌํํ๋ค. ๋ชจ๋ ์ฒ๋ฆฌ๋ ์ยท๋ณ์กฐ ํ์ธ์ด ๊ฐ๋ฅํ ๊ฐ์ฌ ๋ก๊ทธ์ ๋จ๋๋ค.\"\"\"\n",
"import hashlib\n",
"import json\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from matplotlib.path import Path as MplPath\n",
"from scipy import ndimage\n",
"from scipy.spatial import ConvexHull\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ํฉ์ฑ ์ค๋ฃจ์ฃ ์์ฑ๊ธฐ (์ค์ ๊ธฐ์ฒด ์ฌ์งยท๋๋ฉดยท์ ์ ์ฌ์ฉ ์ ํจ)\n",
"# ์ฌ๊ฐํยท์ผ๊ฐํยท์ ๊ฐ์ ๊ธฐ๋ณธ ๋ํ์ ์กฐํฉํ ์ถ์ ์ค๋ฃจ์ฃ์ 32x32 ํ๋ฐฑ ์ด๋ฏธ์ง๋ก ๊ทธ๋ฆฐ๋ค.\n",
"# ํ์ (๋ค๊ฐ๋), ์๊ทผ ์ถ์(์ธก๋ฉด ์์ ), ํฌ๊ธฐ(์๊ฑฐ๋ฆฌ), ํ๋ฆผยท์ก์ยท์ ๋๋นยท๊ตฌ๋ฆยท๊ฐ๋ฆผยท์ผ๊ฐ ๋ฐ์ ์ ์กฐ์ ํ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"IMG = 32\n",
"CLASSES = [\"civil_quad\", \"civil_fw\", \"attack_fw\", \"cruise\", \"ballistic\", \"bird\"]\n",
"KOR = {\"civil_quad\": \"๋ฏผ๊ฐ ์ฟผ๋์ฝฅํฐํ\", \"civil_fw\": \"๋ฏผ๊ฐ ๊ณ ์ ์ตํ\", \"attack_fw\": \"์ผ๊ฐ์ต ์ํญ๋๋ก ํ\",\n",
" \"cruise\": \"์ํญ๋ฏธ์ฌ์ผํ\", \"ballistic\": \"ํ๋๋ฏธ์ฌ์ผํ\", \"bird\": \"์กฐ๋ฅ\"}\n",
"THREAT = {\"attack_fw\", \"cruise\", \"ballistic\"}\n",
"\n",
"\n",
"def _rect(x0, x1, y0, y1):\n",
" return [(x0, y0), (x1, y0), (x1, y1), (x0, y1)]\n",
"\n",
"\n",
"def _bar(p, q, w):\n",
" (x0, y0), (x1, y1) = p, q\n",
" n = np.array([y0 - y1, x1 - x0]) / np.hypot(x1 - x0, y1 - y0) * w / 2\n",
" return [(x0 + n[0], y0 + n[1]), (x1 + n[0], y1 + n[1]), (x1 - n[0], y1 - n[1]), (x0 - n[0], y0 - n[1])]\n",
"\n",
"\n",
"# ๋ํ ์ ์(๋จ์ ์ขํ, ๊ธฐ์ ๋ฐฉํฅ = +y). ๋ชจ๋ ์ถ์ ๋ํ์ด๋ฉฐ ํน์ ๊ธฐ์ข
์ ๋ณธ๋จ์ง ์๋๋ค.\n",
"SHAPES = {\n",
" \"civil_quad\": {\"poly\": [_bar((-.55, -.55), (.55, .55), .1), _bar((-.55, .55), (.55, -.55), .1), _rect(-.15, .15, -.15, .15)],\n",
" \"circ\": [(-.55, -.55, .24), (.55, .55, .24), (-.55, .55, .24), (.55, -.55, .24)]},\n",
" \"civil_fw\": {\"poly\": [_rect(-.07, .07, -.8, .72), _rect(-.9, .9, .05, .25), _rect(-.33, .33, -.8, -.66)], \"circ\": []},\n",
" \"attack_fw\": {\"poly\": [[(0, .85), (.85, -.5), (.12, -.42), (0, -.6), (-.12, -.42), (-.85, -.5)]], \"circ\": []},\n",
" \"cruise\": {\"poly\": [_rect(-.07, .07, -.9, .7), [(-.07, .7), (0, .92), (.07, .7)], _rect(-.42, .42, -.05, .07),\n",
" [(-.07, -.9), (-.24, -.95), (-.07, -.74)], [(.07, -.9), (.24, -.95), (.07, -.74)]], \"circ\": []},\n",
" \"ballistic\": {\"poly\": [_rect(-.13, .13, -.85, .6), [(-.13, .6), (0, .95), (.13, .6)],\n",
" [(-.13, -.85), (-.34, -.95), (-.13, -.58)], [(.13, -.85), (.34, -.95), (.13, -.58)]], \"circ\": []},\n",
" \"bird\": {\"poly\": [[(-.9, .12), (-.5, .28), (-.12, .06), (0, .22), (.12, .06), (.5, .28), (.9, .12), (.5, .12),\n",
" (.12, -.1), (0, -.38), (-.12, -.1), (-.5, .12)]], \"circ\": []},\n",
"}\n",
"_S = 2\n",
"_g = (np.arange(IMG * _S) + 0.5) / _S\n",
"_GX, _GY = np.meshgrid(_g, _g)\n",
"_PTS = np.c_[_GX.ravel(), _GY.ravel()]\n",
"_PATHS = {c: [MplPath(np.array(p)) for p in s[\"poly\"]] for c, s in SHAPES.items()}\n",
"\n",
"\n",
"def draw(cls, rng, angle=None, scale=None, fore=None, blur=0.3, noise=4.0, contrast=1.0,\n",
" clutter=0.0, occlude=0.0, invert=False, shift=1.5):\n",
" \"\"\"ํด๋์ค 1๊ฐ ์ค๋ฃจ์ฃ ์ด๋ฏธ์ง(uint8 32x32)๋ฅผ ๊ทธ๋ฆฐ๋ค. ๋ฐฐ๊ฒฝ์ ๋ฐ์ ํ๋, ๋นํ์ฒด๋ ์ด๋์ด ๊ทธ๋ฆผ์.\"\"\"\n",
" angle = rng.uniform(0, 360) if angle is None else angle\n",
" scale = rng.uniform(.7, .95) if scale is None else scale\n",
" fore = rng.uniform(.85, 1.0) if fore is None else fore\n",
" th = np.radians(angle)\n",
" c0 = IMG / 2 + rng.uniform(-shift, shift, 2)\n",
" d = (_PTS - c0) / (scale * IMG / 2) # ์ด๋ฏธ์ง ์ขํ -> ๋ํ ์ขํ(์ญ๋ณํ)\n",
" u = d[:, 0] * np.cos(th) + d[:, 1] * np.sin(th)\n",
" v = (-d[:, 0] * np.sin(th) + d[:, 1] * np.cos(th)) / fore # ์๊ทผ ์ถ์(๊ธฐ์ ๋ฐฉํฅ์ผ๋ก ์งง์ ๋ณด์)\n",
" uv = np.c_[u, -v]\n",
" m = np.zeros(len(uv), bool)\n",
" near = np.flatnonzero(np.abs(uv).max(axis=1) <= 1.0) # ๋ํ์ด ์์ ์ ์๋ ์์ญ๋ง ๊ฒ์ฌ(์๋)\n",
" for p in _PATHS[cls]:\n",
" m[near] |= p.contains_points(uv[near])\n",
" for cx, cy, r in SHAPES[cls][\"circ\"]:\n",
" m |= (uv[:, 0] - cx) ** 2 + (uv[:, 1] - cy) ** 2 <= r * r\n",
" cov = m.reshape(IMG, _S, IMG, _S).mean(axis=(1, 3))\n",
" if occlude > 0: # ๋ถ๋ถ ๊ฐ๋ฆผ(๊ตฌ๋ฆ ์กฐ๊ฐ)\n",
" h = int(IMG * np.sqrt(occlude)); y0, x0 = rng.integers(8, IMG - 8 - h // 2, 2)\n",
" cov[y0:y0 + h, x0:x0 + h] = 0\n",
" bg = rng.uniform(180, 220) + np.linspace(-10, 10, IMG)[:, None]\n",
" fg = rng.uniform(30, 60)\n",
" img = bg * (1 - cov) + fg * cov\n",
" if clutter > 0: # ๊ตฌ๋ฆยท๋ฐฐ๊ฒฝ ์ผ๋ฃฉ\n",
" img -= clutter * 90 * np.clip(ndimage.gaussian_filter(rng.normal(0, 1, (IMG, IMG)), 3) * 4, 0, None)\n",
" if blur > 0:\n",
" img = ndimage.gaussian_filter(img, blur)\n",
" mean = img.mean()\n",
" img = mean + (img - mean) * contrast\n",
" img = img + rng.normal(0, noise, img.shape)\n",
" if invert: # ์ผ๊ฐ ์ด์(IR) ๊ทน์ฑ ๋ฐ์ \n",
" img = 255 - img\n",
" return np.clip(img, 0, 255).astype(np.uint8)\n",
"\n",
"\n",
"# ์ดฌ์ ์กฐ๊ฑด ํ๋ฆฌ์
: ๊ธฐ๋ณธ + ๋ณตํฉ ์ํ ์๋๋ฆฌ์ค\n",
"CONDITIONS = {\n",
" \"basic\": {},\n",
" \"offcenter\": {\"shift\": 5.0, \"fore\": (.6, .85)},\n",
" \"far\": {\"scale\": (.32, .45), \"blur\": .6},\n",
" \"blur\": {\"blur\": (1.2, 1.8)},\n",
" \"lowcon\": {\"contrast\": (.2, .35)},\n",
" \"clutter\": {\"clutter\": .8},\n",
" \"occlude\": {\"occlude\": .08},\n",
" \"oblique\": {\"fore\": (.35, .6)},\n",
" \"night\": {\"invert\": True, \"noise\": 10.0},\n",
" \"noise\": {\"noise\": 28.0},\n",
" \"combo\": {\"scale\": (.4, .55), \"blur\": (.8, 1.1), \"noise\": 12.0, \"contrast\": (.6, .8)},\n",
"}\n",
"\n",
"\n",
"def draw_cond(cls, cond, rng):\n",
" kw = {}\n",
" for k, v in CONDITIONS[cond].items():\n",
" kw[k] = rng.uniform(*v) if isinstance(v, tuple) else v\n",
" return draw(cls, rng, **kw)\n",
"\n",
"\n",
"def make_dataset(n_per_class, seed, conds=(\"basic\",)):\n",
" \"\"\"(์ด๋ฏธ์ง ๋ฐฐ์ด N x 32 x 32, ์ ๋ต ๋ผ๋ฒจ, ์กฐ๊ฑด) ๋ฐํ. ์กฐ๊ฑด์ conds์์ ๋ฒ๊ฐ์ ๊ณ ๋ฅธ๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" X, y, c = [], [], []\n",
" for cls in CLASSES:\n",
" for i in range(n_per_class):\n",
" cond = conds[i % len(conds)]\n",
" X.append(draw_cond(cls, cond, rng)); y.append(cls); c.append(cond)\n",
" return np.array(X), np.array(y), np.array(c)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์ ์ฒ๋ฆฌ(๋ฐฐ์น ๋ฒกํฐํ)ยทํํ ํน์ง ๊ณตํต ํจ์ ----\n",
"OUT = 24 # ํฌ๊ธฐ ์ ๊ทํ ๊ฒฐ๊ณผ(24x24)\n",
"NOISE_T = 12.0 # ์ก์ ์ถ์ ๊ฐ์ด ์ด๋ณด๋ค ํฌ๋ฉด 3x3 ์ค๊ฐ๊ฐ ํํฐ ์ ์ฉ(์
์ฒํยท์ผ์ ์ก์)\n",
"FEATS = [\"elong\", \"fill\", \"solidity\", \"n_parts\", \"end_gap\", \"mid_bulge\", \"w_max\", \"compact\"]\n",
"\n",
"\n",
"def preprocess(images, noise_t=NOISE_T):\n",
" \"\"\"N์ฅ ์ผ๊ด ์ฒ๋ฆฌ: ๊ฒฐ์ธก ๋ณด์ -> ๊ทน์ฑ(์ผ๊ฐ ๋ฐ์ ) ๋ณด์ -> ์กฐ๊ฑด๋ณ ์ก์ ์ ๊ฑฐ -> ๋ช
์ ๋๋น ๊ฐํ -> ๋ถํ -> ์ค๊ณฝ์ -> ๋ฐฉํฅยทํฌ๊ธฐ ์ ๊ทํ.\"\"\"\n",
" f = np.array(images, dtype=float).reshape(-1, IMG, IMG)\n",
" n = len(f)\n",
" bad = ~np.isfinite(f)\n",
" missing = bad.sum(axis=(1, 2))\n",
" if bad.any(): # ๊ฒฐ์ธก ํฝ์
: ์์ ์ค์๊ฐ์ผ๋ก ์ฑ์ด ๋ค ์ฃผ๋ณ 3x3 ์ค๊ฐ๊ฐ์ผ๋ก ๋ณด์ \n",
" med = np.array([np.median(x[np.isfinite(x)]) if np.isfinite(x).any() else 128.0 for x in f])\n",
" f = np.where(bad, med[:, None, None], f)\n",
" f = np.where(bad, ndimage.median_filter(f, size=(1, 3, 3)), f)\n",
" border = np.concatenate([f[:, 0], f[:, -1], f[:, :, 0], f[:, :, -1]], axis=1)\n",
" inverted = np.median(border, axis=1) < 128 # ๋ฐฐ๊ฒฝ์ด ์ด๋์ฐ๋ฉด ์ผ๊ฐ ์ด์์ผ๋ก ๋ณด๊ณ ๋ฐ์ \n",
" f[inverted] = 255 - f[inverted]\n",
" med3 = ndimage.median_filter(f, size=(1, 3, 3))\n",
" noise = 1.4826 * np.median(np.abs(f - med3).reshape(n, -1), axis=1)\n",
" noisy = noise > noise_t\n",
" f[noisy] = med3[noisy]\n",
" lo, hi = np.percentile(f.reshape(n, -1), [1, 99], axis=1)\n",
" e = np.clip((f - lo[:, None, None]) / np.maximum(hi - lo, 1e-6)[:, None, None], 0, 1) # ๋ช
์ ๋๋น ๊ฐํ\n",
" mask = e < _otsu(e)[:, None, None]\n",
" mask &= ndimage.uniform_filter(mask.astype(float), size=(1, 3, 3)) * 9 >= 2.5 # ๊ณ ๋ฆฝ ์ ์ก์ ์ ๊ฑฐ\n",
" edge = np.hypot(ndimage.sobel(e, axis=1), ndimage.sobel(e, axis=2)) # ์ค๊ณฝ์ (Sobel)\n",
" ok = (mask.sum(axis=(1, 2)) >= 4) & (hi - lo > 8)\n",
" return {\"enh\": e, \"mask\": mask, \"edge\": edge / np.maximum(edge.max(axis=(1, 2)), 1e-6)[:, None, None],\n",
" \"norm\": _normalize(1 - e, mask, ok), \"inverted\": inverted, \"noise\": noise, \"denoised\": noisy,\n",
" \"missing\": missing, \"contrast\": hi - lo, \"ok\": ok}\n",
"\n",
"\n",
"def _otsu(e, bins=64):\n",
" n = len(e)\n",
" idx = np.minimum((e * bins).astype(int), bins - 1) + bins * np.arange(n)[:, None, None]\n",
" hist = np.bincount(idx.ravel(), minlength=bins * n).reshape(n, bins).astype(float)\n",
" centers = (np.arange(bins) + 0.5) / bins\n",
" w = np.cumsum(hist, axis=1); m = np.cumsum(hist * centers, axis=1)\n",
" tot, mt = w[:, -1:], m[:, -1:]\n",
" between = (mt * w / tot - m) ** 2 / np.maximum(w * (tot - w), 1)\n",
" return (np.argmax(between, axis=1) + 1) / bins\n",
"\n",
"\n",
"def _normalize(obj, mask, ok):\n",
" \"\"\"์ฃผ์ถ ๋ฐฉํฅ์ ์ธ๋ก๋ก ๋๋ฆฌ๊ณ ๋นํ์ฒด ์์ญ๋ง 24x24๋ก ๋ค์ ์ํ๋งํ๋ค(ํ์ ยทํฌ๊ธฐยท์์น ๋ถ๋ณ). ํ ๋ฒ์ ๋ณด๊ฐ์ผ๋ก ์ผ๊ด ์ฒ๋ฆฌ.\"\"\"\n",
" n = len(obj)\n",
" w = mask.astype(float)\n",
" m00 = np.maximum(w.sum(axis=(1, 2)), 1)\n",
" yy, xx = np.mgrid[0:IMG, 0:IMG].astype(float)\n",
" cy, cx = (w * yy).sum(axis=(1, 2)) / m00, (w * xx).sum(axis=(1, 2)) / m00\n",
" dy, dx = yy - cy[:, None, None], xx - cx[:, None, None]\n",
" mu20, mu02, mu11 = (w * dy * dy).sum(axis=(1, 2)), (w * dx * dx).sum(axis=(1, 2)), (w * dy * dx).sum(axis=(1, 2))\n",
" th = 0.5 * np.arctan2(2 * mu11, mu20 - mu02)\n",
" a = np.stack([np.cos(th), np.sin(th)], 1) # ์ฃผ์ถ(y, x)\n",
" b = np.stack([-np.sin(th), np.cos(th)], 1) # ๋ถ์ถ\n",
" pa = np.where(mask, dy * a[:, 0, None, None] + dx * a[:, 1, None, None], 0)\n",
" pb = np.where(mask, dy * b[:, 0, None, None] + dx * b[:, 1, None, None], 0)\n",
" half = np.maximum(np.abs(pa).max(axis=(1, 2)), np.abs(pb).max(axis=(1, 2))) + 1.0\n",
" g = np.linspace(-1, 1, OUT)\n",
" U, V = np.meshgrid(g, g, indexing=\"ij\")\n",
" Y = cy[:, None, None] + half[:, None, None] * (U * a[:, 0, None, None] + V * b[:, 0, None, None])\n",
" X = cx[:, None, None] + half[:, None, None] * (U * a[:, 1, None, None] + V * b[:, 1, None, None])\n",
" N = np.broadcast_to(np.arange(n)[:, None, None], Y.shape)\n",
" out = ndimage.map_coordinates(obj * mask, [N, Y, X], order=1, cval=0.0)\n",
" skew = (out * (U ** 3)).sum(axis=(1, 2))\n",
" out[skew < 0] = out[skew < 0, ::-1] # ๋ฌด๊ฒ๊ฐ ์ ๋ฆฐ ์ชฝ์ ์๋ก(180๋ ๋ชจํธ์ฑ ์ ๊ฑฐ)\n",
" out[~ok] = 0\n",
" return np.clip(out, 0, 1)\n",
"\n",
"\n",
"def shape_features(mask):\n",
" \"\"\"๋ถํ ๋ง์คํฌ 1์ฅ์์ ํ์ ์ ๋ฌด๊ดํ ํํ ํน์ง 8์ข
์ ๊ณ์ฐํ๋ค.\"\"\"\n",
" if mask.sum() < 4:\n",
" return dict.fromkeys(FEATS, 0.0)\n",
" yx = np.argwhere(mask).astype(float)\n",
" c = yx - yx.mean(axis=0)\n",
" val, vec = np.linalg.eigh(np.cov(c.T) + 1e-6 * np.eye(2))\n",
" t, s = c @ vec[:, 1], c @ vec[:, 0]\n",
" length, width = np.ptp(t) + 1, np.ptp(s) + 1\n",
" corners = np.vstack([yx + d for d in ([.5, .5], [.5, -.5], [-.5, .5], [-.5, -.5])])\n",
" try:\n",
" hull = ConvexHull(corners).volume\n",
" except Exception:\n",
" hull = float(mask.sum())\n",
" bins = np.digitize(t, np.linspace(t.min(), t.max() + 1e-6, 4)[1:-1])\n",
" w = np.array([np.ptp(s[bins == k]) + 1 if (bins == k).any() else 0 for k in range(3)]) / length\n",
" boundary = mask & ~ndimage.binary_erosion(mask)\n",
" return {\"elong\": float(np.sqrt(val[1] / max(val[0], 1e-6))), \"fill\": float(mask.sum() / (length * width)),\n",
" \"solidity\": float(mask.sum() / max(hull, 1)), \"n_parts\": float(ndimage.label(mask)[1]),\n",
" \"end_gap\": float(abs(w[0] - w[2])), \"mid_bulge\": float(w[1] - max(w[0], w[2])),\n",
" \"w_max\": float(w.max()), \"compact\": float(boundary.sum() / np.sqrt(mask.sum()))}\n",
"\n",
"\n",
"def feature_matrix(images):\n",
" \"\"\"์ ์ฒ๋ฆฌ ๊ฒฐ๊ณผ, ํํ ํน์ง ํ๋ ฌ(N x 8), ์ ๊ทํ ์ด๋ฏธ์ง ํ๋ ฌ(N x 576)์ ๋๋ ค์ค๋ค.\"\"\"\n",
" P = preprocess(images)\n",
" F = np.array([[d[k] for k in FEATS] for d in map(shape_features, P[\"mask\"])])\n",
" return P, F, P[\"norm\"].reshape(len(F), -1)\n",
"\n",
"FEAT_KOR = {\"elong\": \"๊ธธ์ญํจ\", \"fill\": \"์ฑ์๋ฅ \", \"solidity\": \"๋ณผ๋ก๋\", \"n_parts\": \"๋ถ๋ฆฌ ๋ถ๋ถ ์\", \"end_gap\": \"์๋ค ํญ ์ฐจ์ด\",\n",
" \"mid_bulge\": \"์ค์๋ถ ๋ ๊ฐ ๋์ถ\", \"w_max\": \"์ต๋ ํญ(๋ ๊ฐ ํญ)\", \"compact\": \"์ค๊ณฝ ๋ณต์ก๋\"}\n",
"KEY = {\"civil_quad\": \"์ํ ํ์ ๋ ๊ฐ 4๊ฐ\", \"civil_fw\": \"์ง์ ์ฃผ์ต๊ณผ ๊ผฌ๋ฆฌ๋ ๊ฐ\", \"attack_fw\": \"๊ผฌ๋ฆฌ ์๋ ์ผ๊ฐํ ๋ ๊ฐ\",\n",
" \"cruise\": \"๊ฐ๋ ๋์ฒด์ ์์ ์ค๊ฐ ๋ ๊ฐ\", \"ballistic\": \"๊ตต์ ๋์ฒดยท๋พฐ์กฑํ ๋จธ๋ฆฌยท๊ผฌ๋ฆฌ ํ\", \"bird\": \"๊ฐ๋งค๊ธฐํ์ผ๋ก ๊ตฝ์ ๋ ๊ฐ\"}\n",
"ACTIONS = {\"APPROVE\": \"์น์ธ(๊ณต๊ฐ)\", \"EDIT\": \"์์ ํ ์น์ธ\", \"REJECT\": \"๋ฐ๋ ค(์ฌ์์ฑ)\"}\n",
"SLA_H = 24\n",
"\n",
"\n",
"def template(cls):\n",
" return draw(cls, np.random.default_rng(0), angle=0, scale=.85, fore=1.0, blur=0, noise=0, shift=0)\n",
"\n",
"\n",
"TEMPL = {c: template(c) for c in CLASSES}\n",
"TF = {c: shape_features(preprocess([TEMPL[c]])[\"mask\"][0]) for c in CLASSES}\n",
"SPREAD = {k: np.std([TF[c][k] for c in CLASSES]) + 1e-6 for k in FEATS}\n",
"\n",
"\n",
"def draft(answer, chosen):\n",
" \"\"\"AI ํด์ค ์ด์: ๋ ํด๋์ค ๊ธฐ์ค ์ค๋ฃจ์ฃ์ ํํ ํน์ง ์ฐจ์ด ์์ 3๊ฐ๋ฅผ ๋ฌธ์ฅ์ผ๋ก ๋ง๋ ๋ค(์ด์์ผ ๋ฟ ๊ณต๊ฐํ์ง ์์).\"\"\"\n",
" diff = sorted(FEATS, key=lambda k: -abs(TF[answer][k] - TF[chosen][k]) / SPREAD[k])[:3]\n",
" parts = [f\"{FEAT_KOR[k]} {TF[answer][k]:.2f} vs {TF[chosen][k]:.2f}\" for k in diff]\n",
" return f\"์ ๋ต {KOR[answer]}์(๋) {KEY[answer]}, ์ ํํ {KOR[chosen]}์(๋) {KEY[chosen]}. ์ฐจ์ด: \" + \"; \".join(parts)\n",
"\n",
"\n",
"class ReviewQueue:\n",
" def __init__(self):\n",
" self.items, self.log, self.clock = {}, [], 0.0\n",
"\n",
" def _record(self, body):\n",
" prev = self.log[-1][\"hash\"] if self.log else \"GENESIS\"\n",
" body = {**body, \"t_h\": self.clock}\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(self):\n",
" prev = \"GENESIS\"\n",
" for n, r in enumerate(self.log, 1):\n",
" body = {k: v for k, v in r.items() if k not in (\"prev\", \"hash\")}\n",
" if r[\"prev\"] != prev or r[\"hash\"] != hashlib.sha256((prev + json.dumps(body, ensure_ascii=False, sort_keys=True)).encode()).hexdigest():\n",
" return False, n\n",
" prev = r[\"hash\"]\n",
" return True, -1\n",
"\n",
" def submit(self, learner, answer, chosen, freq):\n",
" key = f\"{answer}->{chosen}\"\n",
" if key in self.items and self.items[key][\"status\"] == \"PENDING\": # ๊ฐ์ ํผ๋ ์์ ํ ๊ฑด์ผ๋ก ๋ฌถ๊ณ ๋น๋๋ง ์ฌ๋ฆฐ๋ค\n",
" self.items[key][\"learners\"].append(learner); self.items[key][\"freq\"] = freq\n",
" else:\n",
" self.items[key] = {\"key\": key, \"answer\": answer, \"chosen\": chosen, \"draft\": draft(answer, chosen), \"status\": \"PENDING\",\n",
" \"published\": None, \"learners\": [learner], \"freq\": freq, \"t_in\": self.clock, \"escalated\": False}\n",
" self._record({\"event\": \"SUBMIT\", \"key\": key, \"learner\": learner, \"by\": \"AI\"})\n",
"\n",
" def order(self):\n",
" todo = [i for i in self.items.values() if i[\"status\"] == \"PENDING\"]\n",
" return sorted(todo, key=lambda i: (not i[\"escalated\"], -i[\"freq\"], i[\"t_in\"]))\n",
"\n",
" def decide(self, key, action, instructor, reason, text=None, feedback=\"\"):\n",
" it = self.items.get(key)\n",
" if it is None or it[\"status\"] != \"PENDING\":\n",
" raise ValueError(\"๊ฒํ ๋๊ธฐ ์ค์ธ ํญ๋ชฉ์ด ์๋\")\n",
" if action not in ACTIONS or not instructor.startswith(\"INS\"):\n",
" raise ValueError(\"ํ์ฉ๋์ง ์์ ์กฐ์น ๋๋ ๊ต๊ด ID ์์\")\n",
" if len(reason.strip()) < 5:\n",
" raise ValueError(\"ํ๋จ ๊ทผ๊ฑฐ 5์ ์ด์ ํ์\")\n",
" if action == \"EDIT\" and not text:\n",
" raise ValueError(\"์์ ์น์ธ์ ์์ ํด์ค๋ฌธ ํ์\")\n",
" it[\"status\"] = {\"APPROVE\": \"APPROVED\", \"EDIT\": \"APPROVED\", \"REJECT\": \"REJECTED\"}[action]\n",
" it[\"published\"] = (text or it[\"draft\"]) if action != \"REJECT\" else None\n",
" it[\"wait_h\"] = self.clock - it[\"t_in\"]\n",
" self._record({\"event\": \"DECIDE\", \"key\": key, \"action\": action, \"by\": instructor, \"reason\": reason, \"feedback\": feedback})\n",
"\n",
" def check_sla(self):\n",
" out = []\n",
" for i in self.items.values():\n",
" if i[\"status\"] == \"PENDING\" and not i[\"escalated\"] and self.clock - i[\"t_in\"] > SLA_H:\n",
" i[\"escalated\"] = True; out.append(i[\"key\"])\n",
" self._record({\"event\": \"ESCALATE\", \"key\": i[\"key\"], \"to\": \"๊ต์กํ๋ จ ๋ด๋น๊ด\"})\n",
" return out\n",
"\n",
"\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ํ์ต์ 20๋ช
์ ์ค๋ต ๊ธฐ๋ก(๊ฐ๋ช
L01~L20)\n",
"rng = np.random.default_rng(4)\n",
"pairs = [(\"attack_fw\", \"civil_fw\")] * 6 + [(\"cruise\", \"ballistic\")] * 5 + [(\"civil_fw\", \"attack_fw\")] * 3 + [(\"bird\", \"civil_fw\")] * 2 + [(\"ballistic\", \"cruise\")] * 4\n",
"q = ReviewQueue()\n",
"freq = {}\n",
"for n, (a, c) in enumerate(rng.permutation(np.array(pairs, dtype=object))):\n",
" freq[(a, c)] = freq.get((a, c), 0) + 1\n",
" q.clock += 0.5\n",
" q.submit(f\"L{n + 1:02d}\", a, c, freq[(a, c)])\n",
"print(f\"์ค๋ต {len(pairs)}๊ฑด -> ํด์ค ๊ฒํ ํญ๋ชฉ {len(q.items)}๊ฑด (ํผ๋ ์ ๋จ์), ๊ณต๊ฐ๋ ํด์ค {sum(i['published'] is not None for i in q.items.values())}๊ฑด\")\n",
"print(\"๊ฒํ ์์(๋น๋์):\", [f\"{i['key']}({i['freq']})\" for i in q.order()])\n",
"print(\"\\n[AI ํด์ค ์ด์ ์์ - ๊ต๊ด ๊ฒํ ์ ๋น๊ณต๊ฐ]\\n\" + q.order()[0][\"draft\"])\n",
"\n",
"for args in [(\"attack_fw->civil_fw\", \"APPROVE\", \"\", \"๊ทผ๊ฑฐ ์ถฉ๋ถ\"), (\"attack_fw->civil_fw\", \"EDIT\", \"INS01\", \"ํํ ๋ณด์ ํ์\"), (\"cruise->ballistic\", \"PUBLISH\", \"INS01\", \"์๋ ๊ณต๊ฐ\")]:\n",
" try:\n",
" q.decide(*args)\n",
" except ValueError as e:\n",
" print(\"์ฐจ๋จ:\", e)\n",
"\n",
"q.clock += 2; q.decide(\"attack_fw->civil_fw\", \"EDIT\", \"INS01\", \"๊ผฌ๋ฆฌ๋ ๊ฐ ์ ๋ฌด๋ฅผ ๋จผ์ ๋ณด๋๋ก ์์ \",\n",
" text=\"๊ผฌ๋ฆฌ๋ ๊ฐ๊ฐ ์๊ณ ๋ ๊ฐ๊ฐ ์ผ๊ฐํ์ด๋ฉด ์ํญ๋๋ก ํ, ์ง์ ๋ ๊ฐ์ ๊ผฌ๋ฆฌ๋ ๊ฐ๊ฐ ์์ผ๋ฉด ๋ฏผ๊ฐ ๊ณ ์ ์ตํ์ด๋ค.\", feedback=\"์๊ฑฐ๋ฆฌ ์์ ์ถ๊ฐ ์๋ง\")\n",
"q.clock += 3; q.decide(\"cruise->ballistic\", \"APPROVE\", \"INS02\", \"์ค๊ฐ ๋ ๊ฐ ์ ๋ฌด ์ค๋ช
์ด ์ ํํจ\")\n",
"q.clock += 1; q.decide(\"bird->civil_fw\", \"REJECT\", \"INS02\", \"์กฐ๋ฅ ๋ ๊ฐฏ์ง ๋ณํ ์ค๋ช
๋๋ฝ, ์ฌ์์ฑ\")\n",
"q.clock += 30\n",
"print(\"SLA ์ด๊ณผ ์์ :\", q.check_sla())\n",
"q.clock += 1; q.decide(\"ballistic->cruise\", \"APPROVE\", \"INS01\", \"๋ชธํต ๊ตต๊ธฐ ๋น๊ต๊ฐ ๋ช
ํํจ\")\n",
"\n",
"df = pd.DataFrame(q.items.values())\n",
"expected = len(pairs) + int((df[\"status\"] != \"PENDING\").sum()) + int(df[\"escalated\"].sum()) # ์ ์ถ + ๊ฒฐ์ + ์์ ์ด๋ฒคํธ ์\n",
"print(\"๋จ์ ๊ฒํ ๋๊ธฐ:\", [f\"{i['key']}(๋๊ธฐ {q.clock - i['t_in']:.1f}h)\" for i in q.order()])\n",
"print(\"\\n\" + df[[\"key\", \"freq\", \"status\", \"escalated\", \"wait_h\"]].fillna(\"-\").to_string(index=False))\n",
"print(f\"\\n์๋ ๊ณต๊ฐ 0๊ฑด(์น์ธ ์์ด ๊ณต๊ฐ๋ ํด์ค {sum(1 for i in q.items.values() if i['published'] and i['status'] != 'APPROVED')}๊ฑด), \"\n",
" f\"๊ฐ์ฌ ๋ก๊ทธ {len(q.log)}๊ฑด, ๋ก๊น
์ฑ๊ณต๋ฅ {len(q.log) / expected:.0%}, ํด์ ์ฒด์ธ ๊ฒ์ฆ {q.verify()}\")\n",
"q.log[3][\"learner\"] = \"L99\"\n",
"print(\"4๋ฒ์งธ ๊ธฐ๋ก ๋ณ์กฐ ํ ๊ฒ์ฆ(False, ๊นจ์ง ์์น):\", q.verify())\n",
"print(\"๊ต๊ด ํผ๋๋ฐฑ ์
๋ ฅ ํญ๋ชฉ: ๊ต๊ด ID, ์กฐ์น, ํ๋จ ๊ทผ๊ฑฐ, ์์ ํด์ค๋ฌธ, ์งํ๊ดยท๊ต๊ด ์๊ฒฌ\")\n",
"\n",
"a, c = \"attack_fw\", \"civil_fw\"\n",
"fig, ax = plt.subplots(1, 3, figsize=(10, 3.6))\n",
"ax[0].imshow(TEMPL[a], cmap=\"gray\"); ax[0].set_title(f\"correct: {a}\")\n",
"ax[1].imshow(TEMPL[c], cmap=\"gray\"); ax[1].set_title(f\"chosen: {c}\")\n",
"ax[2].imshow((TEMPL[c].astype(float) - TEMPL[a]), cmap=\"coolwarm\"); ax[2].set_title(\"difference (red = only correct)\")\n",
"for x in ax:\n",
" x.axis(\"off\")\n",
"plt.suptitle(\"Wrong-answer comparison for instructor review (synthetic)\"); plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
07 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"์ค๋ต 20๊ฑด -> ํด์ค ๊ฒํ ํญ๋ชฉ 5๊ฑด (ํผ๋ ์ ๋จ์), ๊ณต๊ฐ๋ ํด์ค 0๊ฑด\n",
"๊ฒํ ์์(๋น๋์): ['attack_fw->civil_fw(6)', 'cruise->ballistic(5)', 'ballistic->cruise(4)', 'civil_fw->attack_fw(3)', 'bird->civil_fw(2)']\n",
"\n",
"[AI ํด์ค ์ด์ ์์ - ๊ต๊ด ๊ฒํ ์ ๋น๊ณต๊ฐ]\n",
"์ ๋ต ์ผ๊ฐ์ต ์ํญ๋๋ก ํ์(๋) ๊ผฌ๋ฆฌ ์๋ ์ผ๊ฐํ ๋ ๊ฐ, ์ ํํ ๋ฏผ๊ฐ ๊ณ ์ ์ตํ์(๋) ์ง์ ์ฃผ์ต๊ณผ ๊ผฌ๋ฆฌ๋ ๊ฐ. ์ฐจ์ด: ์ค๊ณฝ ๋ณต์ก๋ 3.82 vs 7.95; ๋ณผ๋ก๋ 0.86 vs 0.40; ์ฑ์๋ฅ 0.51 vs 0.29\n",
"์ฐจ๋จ: ํ์ฉ๋์ง ์์ ์กฐ์น ๋๋ ๊ต๊ด ID ์์\n",
"์ฐจ๋จ: ์์ ์น์ธ์ ์์ ํด์ค๋ฌธ ํ์\n",
"์ฐจ๋จ: ํ์ฉ๋์ง ์์ ์กฐ์น ๋๋ ๊ต๊ด ID ์์\n",
"SLA ์ด๊ณผ ์์ : ['civil_fw->attack_fw', 'ballistic->cruise']\n",
"๋จ์ ๊ฒํ ๋๊ธฐ: ['civil_fw->attack_fw(๋๊ธฐ 46.0h)']\n",
"\n",
" key freq status escalated wait_h\n",
"attack_fw->civil_fw 6 APPROVED False 11.5\n",
"civil_fw->attack_fw 3 PENDING True -\n",
" cruise->ballistic 5 APPROVED False 13.5\n",
" ballistic->cruise 4 APPROVED True 44.5\n",
" bird->civil_fw 2 REJECTED False 10.0\n",
"\n",
"์๋ ๊ณต๊ฐ 0๊ฑด(์น์ธ ์์ด ๊ณต๊ฐ๋ ํด์ค 0๊ฑด), ๊ฐ์ฌ ๋ก๊ทธ 26๊ฑด, ๋ก๊น
์ฑ๊ณต๋ฅ 100%, ํด์ ์ฒด์ธ ๊ฒ์ฆ (True, -1)\n",
"4๋ฒ์งธ ๊ธฐ๋ก ๋ณ์กฐ ํ ๊ฒ์ฆ(False, ๊นจ์ง ์์น): (False, 4)\n",
"๊ต๊ด ํผ๋๋ฐฑ ์
๋ ฅ ํญ๋ชฉ: ๊ต๊ด ID, ์กฐ์น, ํ๋จ ๊ทผ๊ฑฐ, ์์ ํด์ค๋ฌธ, ์งํ๊ดยท๊ต๊ด ์๊ฒฌ\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
08. ๋ค๋์ ์ ํ์ง ํ๋ฆฐ ์ค๋ฃจ์ฃ ์ด๋ฏธ์ง ๋ถ๋ฅ ์คํธ๋ ์ค ํ
์คํธ ๋ฐ ํ๊ณ์น ๊ฒ์ฆ\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"ํ๋ฆฐ ์ ํ์ง ์ค๋ฃจ์ฃ ์ด๋ฏธ์ง ๋๋ ๋ถ๋ฅ ์คํธ๋ ์ค ํ
์คํธ๋ฅผ ์ํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๋ค๋์ ํ๋ฆฌ๊ณ ํ์ง์ด ๋ฎ์ ์ค๋ฃจ์ฃ ์ด๋ฏธ์ง๊ฐ ํ๊บผ๋ฒ์ ๋ค์ด์ฌ ๋ ๋ถ๋ฅ ์๋น์ค๊ฐ ๋ฉ์ถ์ง ์๊ณ ์์ ํ๊ฒ ๋์ํ๋์ง ํ๊ณ์น๋ฅผ ๊ฒ์ฆํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **ํ์ 100์ฅ์์ 500% ์ฆ๊ฐํ 600์ฅ๊น์ง ์์ฒญ์ ๋๋ฆฌ๊ณ , ์์ ์
๋ ฅ๊ณผ ๊ทน์ ํ์ง ์์์ ์์ด ์์คํ
๋ค์ดยท๊ฒฐ๊ณผ ์ ์ค ์ฌ๋ถ ํ์ธํ๊ณ ์ ํจ์ฑ์ ์ ๊ฒํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `tests/trn03_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": [
"\"\"\"ํ๋ฆฌ๊ณ ํ์ง์ด ๋ฎ์ ์ค๋ฃจ์ฃ ์ด๋ฏธ์ง๋ฅผ ํ์์ ์ต๋ 6๋ฐฐ(500% ์ฆ๊ฐ)๊น์ง ํ๊บผ๋ฒ์ ๋ฃ๊ณ ์์ ์
๋ ฅ์ ์์ด, ๋ถ๋ฅ ์๋น์ค๊ฐ ๋ฉ์ถ์ง ์๋์ง(๋ค์ด 0๊ฑด)ยท๊ฒฐ๊ณผ๊ฐ ๋น ์ง์ง ์๋์ง(์ ์ค๋ฅ 0%)ยทํ๋ฆผ ํ๊ณ์นยท์ฒ๋ฆฌ๋ยท๋ฉ๋ชจ๋ฆฌยท๋ณ๋ชฉ ๊ตฌ๊ฐ์ ๊ฒ์ฆํ๋ค.\"\"\"\n",
"import time\n",
"import tracemalloc\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from matplotlib.path import Path as MplPath\n",
"from scipy import ndimage\n",
"from scipy.spatial import ConvexHull\n",
"from sklearn.neural_network import MLPClassifier\n",
"from sklearn.preprocessing import StandardScaler\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ํฉ์ฑ ์ค๋ฃจ์ฃ ์์ฑ๊ธฐ (์ค์ ๊ธฐ์ฒด ์ฌ์งยท๋๋ฉดยท์ ์ ์ฌ์ฉ ์ ํจ)\n",
"# ์ฌ๊ฐํยท์ผ๊ฐํยท์ ๊ฐ์ ๊ธฐ๋ณธ ๋ํ์ ์กฐํฉํ ์ถ์ ์ค๋ฃจ์ฃ์ 32x32 ํ๋ฐฑ ์ด๋ฏธ์ง๋ก ๊ทธ๋ฆฐ๋ค.\n",
"# ํ์ (๋ค๊ฐ๋), ์๊ทผ ์ถ์(์ธก๋ฉด ์์ ), ํฌ๊ธฐ(์๊ฑฐ๋ฆฌ), ํ๋ฆผยท์ก์ยท์ ๋๋นยท๊ตฌ๋ฆยท๊ฐ๋ฆผยท์ผ๊ฐ ๋ฐ์ ์ ์กฐ์ ํ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"IMG = 32\n",
"CLASSES = [\"civil_quad\", \"civil_fw\", \"attack_fw\", \"cruise\", \"ballistic\", \"bird\"]\n",
"KOR = {\"civil_quad\": \"๋ฏผ๊ฐ ์ฟผ๋์ฝฅํฐํ\", \"civil_fw\": \"๋ฏผ๊ฐ ๊ณ ์ ์ตํ\", \"attack_fw\": \"์ผ๊ฐ์ต ์ํญ๋๋ก ํ\",\n",
" \"cruise\": \"์ํญ๋ฏธ์ฌ์ผํ\", \"ballistic\": \"ํ๋๋ฏธ์ฌ์ผํ\", \"bird\": \"์กฐ๋ฅ\"}\n",
"THREAT = {\"attack_fw\", \"cruise\", \"ballistic\"}\n",
"\n",
"\n",
"def _rect(x0, x1, y0, y1):\n",
" return [(x0, y0), (x1, y0), (x1, y1), (x0, y1)]\n",
"\n",
"\n",
"def _bar(p, q, w):\n",
" (x0, y0), (x1, y1) = p, q\n",
" n = np.array([y0 - y1, x1 - x0]) / np.hypot(x1 - x0, y1 - y0) * w / 2\n",
" return [(x0 + n[0], y0 + n[1]), (x1 + n[0], y1 + n[1]), (x1 - n[0], y1 - n[1]), (x0 - n[0], y0 - n[1])]\n",
"\n",
"\n",
"# ๋ํ ์ ์(๋จ์ ์ขํ, ๊ธฐ์ ๋ฐฉํฅ = +y). ๋ชจ๋ ์ถ์ ๋ํ์ด๋ฉฐ ํน์ ๊ธฐ์ข
์ ๋ณธ๋จ์ง ์๋๋ค.\n",
"SHAPES = {\n",
" \"civil_quad\": {\"poly\": [_bar((-.55, -.55), (.55, .55), .1), _bar((-.55, .55), (.55, -.55), .1), _rect(-.15, .15, -.15, .15)],\n",
" \"circ\": [(-.55, -.55, .24), (.55, .55, .24), (-.55, .55, .24), (.55, -.55, .24)]},\n",
" \"civil_fw\": {\"poly\": [_rect(-.07, .07, -.8, .72), _rect(-.9, .9, .05, .25), _rect(-.33, .33, -.8, -.66)], \"circ\": []},\n",
" \"attack_fw\": {\"poly\": [[(0, .85), (.85, -.5), (.12, -.42), (0, -.6), (-.12, -.42), (-.85, -.5)]], \"circ\": []},\n",
" \"cruise\": {\"poly\": [_rect(-.07, .07, -.9, .7), [(-.07, .7), (0, .92), (.07, .7)], _rect(-.42, .42, -.05, .07),\n",
" [(-.07, -.9), (-.24, -.95), (-.07, -.74)], [(.07, -.9), (.24, -.95), (.07, -.74)]], \"circ\": []},\n",
" \"ballistic\": {\"poly\": [_rect(-.13, .13, -.85, .6), [(-.13, .6), (0, .95), (.13, .6)],\n",
" [(-.13, -.85), (-.34, -.95), (-.13, -.58)], [(.13, -.85), (.34, -.95), (.13, -.58)]], \"circ\": []},\n",
" \"bird\": {\"poly\": [[(-.9, .12), (-.5, .28), (-.12, .06), (0, .22), (.12, .06), (.5, .28), (.9, .12), (.5, .12),\n",
" (.12, -.1), (0, -.38), (-.12, -.1), (-.5, .12)]], \"circ\": []},\n",
"}\n",
"_S = 2\n",
"_g = (np.arange(IMG * _S) + 0.5) / _S\n",
"_GX, _GY = np.meshgrid(_g, _g)\n",
"_PTS = np.c_[_GX.ravel(), _GY.ravel()]\n",
"_PATHS = {c: [MplPath(np.array(p)) for p in s[\"poly\"]] for c, s in SHAPES.items()}\n",
"\n",
"\n",
"def draw(cls, rng, angle=None, scale=None, fore=None, blur=0.3, noise=4.0, contrast=1.0,\n",
" clutter=0.0, occlude=0.0, invert=False, shift=1.5):\n",
" \"\"\"ํด๋์ค 1๊ฐ ์ค๋ฃจ์ฃ ์ด๋ฏธ์ง(uint8 32x32)๋ฅผ ๊ทธ๋ฆฐ๋ค. ๋ฐฐ๊ฒฝ์ ๋ฐ์ ํ๋, ๋นํ์ฒด๋ ์ด๋์ด ๊ทธ๋ฆผ์.\"\"\"\n",
" angle = rng.uniform(0, 360) if angle is None else angle\n",
" scale = rng.uniform(.7, .95) if scale is None else scale\n",
" fore = rng.uniform(.85, 1.0) if fore is None else fore\n",
" th = np.radians(angle)\n",
" c0 = IMG / 2 + rng.uniform(-shift, shift, 2)\n",
" d = (_PTS - c0) / (scale * IMG / 2) # ์ด๋ฏธ์ง ์ขํ -> ๋ํ ์ขํ(์ญ๋ณํ)\n",
" u = d[:, 0] * np.cos(th) + d[:, 1] * np.sin(th)\n",
" v = (-d[:, 0] * np.sin(th) + d[:, 1] * np.cos(th)) / fore # ์๊ทผ ์ถ์(๊ธฐ์ ๋ฐฉํฅ์ผ๋ก ์งง์ ๋ณด์)\n",
" uv = np.c_[u, -v]\n",
" m = np.zeros(len(uv), bool)\n",
" near = np.flatnonzero(np.abs(uv).max(axis=1) <= 1.0) # ๋ํ์ด ์์ ์ ์๋ ์์ญ๋ง ๊ฒ์ฌ(์๋)\n",
" for p in _PATHS[cls]:\n",
" m[near] |= p.contains_points(uv[near])\n",
" for cx, cy, r in SHAPES[cls][\"circ\"]:\n",
" m |= (uv[:, 0] - cx) ** 2 + (uv[:, 1] - cy) ** 2 <= r * r\n",
" cov = m.reshape(IMG, _S, IMG, _S).mean(axis=(1, 3))\n",
" if occlude > 0: # ๋ถ๋ถ ๊ฐ๋ฆผ(๊ตฌ๋ฆ ์กฐ๊ฐ)\n",
" h = int(IMG * np.sqrt(occlude)); y0, x0 = rng.integers(8, IMG - 8 - h // 2, 2)\n",
" cov[y0:y0 + h, x0:x0 + h] = 0\n",
" bg = rng.uniform(180, 220) + np.linspace(-10, 10, IMG)[:, None]\n",
" fg = rng.uniform(30, 60)\n",
" img = bg * (1 - cov) + fg * cov\n",
" if clutter > 0: # ๊ตฌ๋ฆยท๋ฐฐ๊ฒฝ ์ผ๋ฃฉ\n",
" img -= clutter * 90 * np.clip(ndimage.gaussian_filter(rng.normal(0, 1, (IMG, IMG)), 3) * 4, 0, None)\n",
" if blur > 0:\n",
" img = ndimage.gaussian_filter(img, blur)\n",
" mean = img.mean()\n",
" img = mean + (img - mean) * contrast\n",
" img = img + rng.normal(0, noise, img.shape)\n",
" if invert: # ์ผ๊ฐ ์ด์(IR) ๊ทน์ฑ ๋ฐ์ \n",
" img = 255 - img\n",
" return np.clip(img, 0, 255).astype(np.uint8)\n",
"\n",
"\n",
"# ์ดฌ์ ์กฐ๊ฑด ํ๋ฆฌ์
: ๊ธฐ๋ณธ + ๋ณตํฉ ์ํ ์๋๋ฆฌ์ค\n",
"CONDITIONS = {\n",
" \"basic\": {},\n",
" \"offcenter\": {\"shift\": 5.0, \"fore\": (.6, .85)},\n",
" \"far\": {\"scale\": (.32, .45), \"blur\": .6},\n",
" \"blur\": {\"blur\": (1.2, 1.8)},\n",
" \"lowcon\": {\"contrast\": (.2, .35)},\n",
" \"clutter\": {\"clutter\": .8},\n",
" \"occlude\": {\"occlude\": .08},\n",
" \"oblique\": {\"fore\": (.35, .6)},\n",
" \"night\": {\"invert\": True, \"noise\": 10.0},\n",
" \"noise\": {\"noise\": 28.0},\n",
" \"combo\": {\"scale\": (.4, .55), \"blur\": (.8, 1.1), \"noise\": 12.0, \"contrast\": (.6, .8)},\n",
"}\n",
"\n",
"\n",
"def draw_cond(cls, cond, rng):\n",
" kw = {}\n",
" for k, v in CONDITIONS[cond].items():\n",
" kw[k] = rng.uniform(*v) if isinstance(v, tuple) else v\n",
" return draw(cls, rng, **kw)\n",
"\n",
"\n",
"def make_dataset(n_per_class, seed, conds=(\"basic\",)):\n",
" \"\"\"(์ด๋ฏธ์ง ๋ฐฐ์ด N x 32 x 32, ์ ๋ต ๋ผ๋ฒจ, ์กฐ๊ฑด) ๋ฐํ. ์กฐ๊ฑด์ conds์์ ๋ฒ๊ฐ์ ๊ณ ๋ฅธ๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" X, y, c = [], [], []\n",
" for cls in CLASSES:\n",
" for i in range(n_per_class):\n",
" cond = conds[i % len(conds)]\n",
" X.append(draw_cond(cls, cond, rng)); y.append(cls); c.append(cond)\n",
" return np.array(X), np.array(y), np.array(c)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์ ์ฒ๋ฆฌ(๋ฐฐ์น ๋ฒกํฐํ)ยทํํ ํน์ง ๊ณตํต ํจ์ ----\n",
"OUT = 24 # ํฌ๊ธฐ ์ ๊ทํ ๊ฒฐ๊ณผ(24x24)\n",
"NOISE_T = 12.0 # ์ก์ ์ถ์ ๊ฐ์ด ์ด๋ณด๋ค ํฌ๋ฉด 3x3 ์ค๊ฐ๊ฐ ํํฐ ์ ์ฉ(์
์ฒํยท์ผ์ ์ก์)\n",
"FEATS = [\"elong\", \"fill\", \"solidity\", \"n_parts\", \"end_gap\", \"mid_bulge\", \"w_max\", \"compact\"]\n",
"\n",
"\n",
"def preprocess(images, noise_t=NOISE_T):\n",
" \"\"\"N์ฅ ์ผ๊ด ์ฒ๋ฆฌ: ๊ฒฐ์ธก ๋ณด์ -> ๊ทน์ฑ(์ผ๊ฐ ๋ฐ์ ) ๋ณด์ -> ์กฐ๊ฑด๋ณ ์ก์ ์ ๊ฑฐ -> ๋ช
์ ๋๋น ๊ฐํ -> ๋ถํ -> ์ค๊ณฝ์ -> ๋ฐฉํฅยทํฌ๊ธฐ ์ ๊ทํ.\"\"\"\n",
" f = np.array(images, dtype=float).reshape(-1, IMG, IMG)\n",
" n = len(f)\n",
" bad = ~np.isfinite(f)\n",
" missing = bad.sum(axis=(1, 2))\n",
" if bad.any(): # ๊ฒฐ์ธก ํฝ์
: ์์ ์ค์๊ฐ์ผ๋ก ์ฑ์ด ๋ค ์ฃผ๋ณ 3x3 ์ค๊ฐ๊ฐ์ผ๋ก ๋ณด์ \n",
" med = np.array([np.median(x[np.isfinite(x)]) if np.isfinite(x).any() else 128.0 for x in f])\n",
" f = np.where(bad, med[:, None, None], f)\n",
" f = np.where(bad, ndimage.median_filter(f, size=(1, 3, 3)), f)\n",
" border = np.concatenate([f[:, 0], f[:, -1], f[:, :, 0], f[:, :, -1]], axis=1)\n",
" inverted = np.median(border, axis=1) < 128 # ๋ฐฐ๊ฒฝ์ด ์ด๋์ฐ๋ฉด ์ผ๊ฐ ์ด์์ผ๋ก ๋ณด๊ณ ๋ฐ์ \n",
" f[inverted] = 255 - f[inverted]\n",
" med3 = ndimage.median_filter(f, size=(1, 3, 3))\n",
" noise = 1.4826 * np.median(np.abs(f - med3).reshape(n, -1), axis=1)\n",
" noisy = noise > noise_t\n",
" f[noisy] = med3[noisy]\n",
" lo, hi = np.percentile(f.reshape(n, -1), [1, 99], axis=1)\n",
" e = np.clip((f - lo[:, None, None]) / np.maximum(hi - lo, 1e-6)[:, None, None], 0, 1) # ๋ช
์ ๋๋น ๊ฐํ\n",
" mask = e < _otsu(e)[:, None, None]\n",
" mask &= ndimage.uniform_filter(mask.astype(float), size=(1, 3, 3)) * 9 >= 2.5 # ๊ณ ๋ฆฝ ์ ์ก์ ์ ๊ฑฐ\n",
" edge = np.hypot(ndimage.sobel(e, axis=1), ndimage.sobel(e, axis=2)) # ์ค๊ณฝ์ (Sobel)\n",
" ok = (mask.sum(axis=(1, 2)) >= 4) & (hi - lo > 8)\n",
" return {\"enh\": e, \"mask\": mask, \"edge\": edge / np.maximum(edge.max(axis=(1, 2)), 1e-6)[:, None, None],\n",
" \"norm\": _normalize(1 - e, mask, ok), \"inverted\": inverted, \"noise\": noise, \"denoised\": noisy,\n",
" \"missing\": missing, \"contrast\": hi - lo, \"ok\": ok}\n",
"\n",
"\n",
"def _otsu(e, bins=64):\n",
" n = len(e)\n",
" idx = np.minimum((e * bins).astype(int), bins - 1) + bins * np.arange(n)[:, None, None]\n",
" hist = np.bincount(idx.ravel(), minlength=bins * n).reshape(n, bins).astype(float)\n",
" centers = (np.arange(bins) + 0.5) / bins\n",
" w = np.cumsum(hist, axis=1); m = np.cumsum(hist * centers, axis=1)\n",
" tot, mt = w[:, -1:], m[:, -1:]\n",
" between = (mt * w / tot - m) ** 2 / np.maximum(w * (tot - w), 1)\n",
" return (np.argmax(between, axis=1) + 1) / bins\n",
"\n",
"\n",
"def _normalize(obj, mask, ok):\n",
" \"\"\"์ฃผ์ถ ๋ฐฉํฅ์ ์ธ๋ก๋ก ๋๋ฆฌ๊ณ ๋นํ์ฒด ์์ญ๋ง 24x24๋ก ๋ค์ ์ํ๋งํ๋ค(ํ์ ยทํฌ๊ธฐยท์์น ๋ถ๋ณ). ํ ๋ฒ์ ๋ณด๊ฐ์ผ๋ก ์ผ๊ด ์ฒ๋ฆฌ.\"\"\"\n",
" n = len(obj)\n",
" w = mask.astype(float)\n",
" m00 = np.maximum(w.sum(axis=(1, 2)), 1)\n",
" yy, xx = np.mgrid[0:IMG, 0:IMG].astype(float)\n",
" cy, cx = (w * yy).sum(axis=(1, 2)) / m00, (w * xx).sum(axis=(1, 2)) / m00\n",
" dy, dx = yy - cy[:, None, None], xx - cx[:, None, None]\n",
" mu20, mu02, mu11 = (w * dy * dy).sum(axis=(1, 2)), (w * dx * dx).sum(axis=(1, 2)), (w * dy * dx).sum(axis=(1, 2))\n",
" th = 0.5 * np.arctan2(2 * mu11, mu20 - mu02)\n",
" a = np.stack([np.cos(th), np.sin(th)], 1) # ์ฃผ์ถ(y, x)\n",
" b = np.stack([-np.sin(th), np.cos(th)], 1) # ๋ถ์ถ\n",
" pa = np.where(mask, dy * a[:, 0, None, None] + dx * a[:, 1, None, None], 0)\n",
" pb = np.where(mask, dy * b[:, 0, None, None] + dx * b[:, 1, None, None], 0)\n",
" half = np.maximum(np.abs(pa).max(axis=(1, 2)), np.abs(pb).max(axis=(1, 2))) + 1.0\n",
" g = np.linspace(-1, 1, OUT)\n",
" U, V = np.meshgrid(g, g, indexing=\"ij\")\n",
" Y = cy[:, None, None] + half[:, None, None] * (U * a[:, 0, None, None] + V * b[:, 0, None, None])\n",
" X = cx[:, None, None] + half[:, None, None] * (U * a[:, 1, None, None] + V * b[:, 1, None, None])\n",
" N = np.broadcast_to(np.arange(n)[:, None, None], Y.shape)\n",
" out = ndimage.map_coordinates(obj * mask, [N, Y, X], order=1, cval=0.0)\n",
" skew = (out * (U ** 3)).sum(axis=(1, 2))\n",
" out[skew < 0] = out[skew < 0, ::-1] # ๋ฌด๊ฒ๊ฐ ์ ๋ฆฐ ์ชฝ์ ์๋ก(180๋ ๋ชจํธ์ฑ ์ ๊ฑฐ)\n",
" out[~ok] = 0\n",
" return np.clip(out, 0, 1)\n",
"\n",
"\n",
"def shape_features(mask):\n",
" \"\"\"๋ถํ ๋ง์คํฌ 1์ฅ์์ ํ์ ์ ๋ฌด๊ดํ ํํ ํน์ง 8์ข
์ ๊ณ์ฐํ๋ค.\"\"\"\n",
" if mask.sum() < 4:\n",
" return dict.fromkeys(FEATS, 0.0)\n",
" yx = np.argwhere(mask).astype(float)\n",
" c = yx - yx.mean(axis=0)\n",
" val, vec = np.linalg.eigh(np.cov(c.T) + 1e-6 * np.eye(2))\n",
" t, s = c @ vec[:, 1], c @ vec[:, 0]\n",
" length, width = np.ptp(t) + 1, np.ptp(s) + 1\n",
" corners = np.vstack([yx + d for d in ([.5, .5], [.5, -.5], [-.5, .5], [-.5, -.5])])\n",
" try:\n",
" hull = ConvexHull(corners).volume\n",
" except Exception:\n",
" hull = float(mask.sum())\n",
" bins = np.digitize(t, np.linspace(t.min(), t.max() + 1e-6, 4)[1:-1])\n",
" w = np.array([np.ptp(s[bins == k]) + 1 if (bins == k).any() else 0 for k in range(3)]) / length\n",
" boundary = mask & ~ndimage.binary_erosion(mask)\n",
" return {\"elong\": float(np.sqrt(val[1] / max(val[0], 1e-6))), \"fill\": float(mask.sum() / (length * width)),\n",
" \"solidity\": float(mask.sum() / max(hull, 1)), \"n_parts\": float(ndimage.label(mask)[1]),\n",
" \"end_gap\": float(abs(w[0] - w[2])), \"mid_bulge\": float(w[1] - max(w[0], w[2])),\n",
" \"w_max\": float(w.max()), \"compact\": float(boundary.sum() / np.sqrt(mask.sum()))}\n",
"\n",
"\n",
"def feature_matrix(images):\n",
" \"\"\"์ ์ฒ๋ฆฌ ๊ฒฐ๊ณผ, ํํ ํน์ง ํ๋ ฌ(N x 8), ์ ๊ทํ ์ด๋ฏธ์ง ํ๋ ฌ(N x 576)์ ๋๋ ค์ค๋ค.\"\"\"\n",
" P = preprocess(images)\n",
" F = np.array([[d[k] for k in FEATS] for d in map(shape_features, P[\"mask\"])])\n",
" return P, F, P[\"norm\"].reshape(len(F), -1)\n",
"\n",
"# ---- ๋ถ๋ฅ ๋ชจ๋ธ ๊ณตํต ํจ์(๋ค์ธต ํผ์
ํธ๋ก : ์ ๊ทํ ์ด๋ฏธ์ง 576ํฝ์
+ ํํ ํน์ง 8์ข
) ----\n",
"def to_input(F, N, scaler):\n",
" return np.c_[N, scaler.transform(F) * 0.3]\n",
"\n",
"\n",
"def train_model(n_per_class=150, seed=5, conds=tuple(CONDITIONS), hidden=(64,)):\n",
" X, y, _ = make_dataset(n_per_class, seed, conds)\n",
" P, F, N = feature_matrix(X)\n",
" scaler = StandardScaler().fit(F)\n",
" model = MLPClassifier(hidden, max_iter=200, early_stopping=True, random_state=0)\n",
" model.fit(to_input(F, N, scaler), [CLASSES.index(v) for v in y])\n",
" return model, scaler\n",
"\n",
"\n",
"def predict(model, scaler, images):\n",
" \"\"\"(์ ์ฒ๋ฆฌ ๊ฒฐ๊ณผ, ํด๋์ค๋ณ ํ๋ฅ N x 6)์ ๋๋ ค์ค๋ค.\"\"\"\n",
" P, F, N = feature_matrix(images)\n",
" return P, model.predict_proba(to_input(F, N, scaler))\n",
"\n",
"CAPACITY = 200 # ํ ๋ฒ์ ์ฒ๋ฆฌํ๋ ์ต๋ ์ฅ์(์ด๊ณผ ์ ๋๊ธฐ์ด๋ก ๋๋ ์ฒ๋ฆฌ = ์ฑ๋ฅ์ ํ ๋ชจ๋)\n",
"MIN_CONF = 0.5 # ์ด๋ณด๋ค ๋ฎ์ผ๋ฉด ์ ๋ต์ ๋จ์ ํ์ง ์๊ณ 'ํ๋ณ ๋ถ๊ฐ(๊ต๊ด ํ์ธ)'\n",
"\n",
"\n",
"def serve(requests, stage):\n",
" \"\"\"์์ฒญ ์ ๋ถ์ ๊ฒฐ๊ณผ๋ฅผ ํ๋์ฉ ๋๋ ค์ค๋ค. ์์ ์
๋ ฅ์ '์
๋ ฅ ์ค๋ฅ', ์ ์ ๋ขฐ๋ 'ํ๋ณ ๋ถ๊ฐ'๋ก ์์ ํ๊ฒ ์ฒ๋ฆฌํ๋ค.\"\"\"\n",
" results, valid = {}, []\n",
" for rid, img in requests:\n",
" a = np.asarray(img, dtype=float) if img is not None else None\n",
" if a is None or a.shape != (IMG, IMG) or np.isnan(a).mean() > 0.5:\n",
" results[rid] = \"์
๋ ฅ ์ค๋ฅ(์ฌ์ ์ก ์์ฒญ)\"\n",
" else:\n",
" valid.append((rid, a))\n",
" for k in range(0, len(valid), CAPACITY): # ๋๊ธฐ์ด ์ฒญํฌ ์ฒ๋ฆฌ\n",
" ids, imgs = zip(*valid[k:k + CAPACITY])\n",
" t0 = time.perf_counter(); P = preprocess(np.array(imgs)); t1 = time.perf_counter()\n",
" F = np.array([[d[f] for f in FEATS] for d in map(shape_features, P[\"mask\"])]); t2 = time.perf_counter()\n",
" prob = model.predict_proba(to_input(F, P[\"norm\"].reshape(len(F), -1), scaler)); t3 = time.perf_counter()\n",
" stage[\"์ ์ฒ๋ฆฌ\"] += t1 - t0; stage[\"ํํํน์ง\"] += t2 - t1; stage[\"์ถ๋ก \"] += t3 - t2\n",
" for rid, ok, p in zip(ids, P[\"ok\"], prob):\n",
" results[rid] = CLASSES[int(p.argmax())] if ok and p.max() >= MIN_CONF else \"ํ๋ณ ๋ถ๊ฐ(๊ต๊ด ํ์ธ)\"\n",
" return results\n",
"\n",
"\n",
"def load_case(n, seed, blur=(1.2, 2.2), corrupt=0.0):\n",
" rng = np.random.default_rng(seed)\n",
" y = rng.choice(CLASSES, n)\n",
" reqs = [(f\"R{i:05d}\", draw(c, rng, blur=rng.uniform(*blur), contrast=rng.uniform(.5, 1), noise=10)) for i, c in enumerate(y)]\n",
" for i in rng.choice(n, int(n * corrupt), replace=False):\n",
" reqs[i] = (reqs[i][0], [None, np.full((IMG, IMG), np.nan), np.zeros((20, 20))][i % 3])\n",
" return reqs, dict(zip([r[0] for r in reqs], y))\n",
"\n",
"\n",
"model, scaler = train_model(120, seed=5)\n",
"rows, stage = [], {\"์ ์ฒ๋ฆฌ\": 0.0, \"ํํํน์ง\": 0.0, \"์ถ๋ก \": 0.0}\n",
"for name, n, kw in [(\"ํ์ 100์ฅ\", 100, {}), (\"300% ์ฆ๊ฐ 400์ฅ\", 400, {}), (\"500% ์ฆ๊ฐ 600์ฅ\", 600, {}),\n",
" (\"500% + ์์ ์
๋ ฅ 3%\", 600, {\"corrupt\": .03}), (\"500% + ๊ทน์ ํ์ง(ํ๋ฆผ 3.0)\", 600, {\"blur\": (2.8, 3.2)})]:\n",
" reqs, truth = load_case(n, len(rows) + 1, **kw)\n",
" t0 = time.perf_counter()\n",
" try:\n",
" res, down = serve(reqs, stage), 0\n",
" except Exception as e:\n",
" res, down = {}, 1; print(\"๋ค์ด:\", e)\n",
" dt = time.perf_counter() - t0\n",
" judged = [r for r, v in res.items() if v in CLASSES]\n",
" rows.append({\"์ํฉ\": name, \"์์ฒญ\": n, \"์๋ต\": len(res), \"์ ์ค๋ฅ \": 1 - len(res) / n, \"๋ค์ด\": down,\n",
" \"ํ๋ณ๋ถ๊ฐ\": sum(v.startswith(\"ํ๋ณ\") for v in res.values()), \"์
๋ ฅ์ค๋ฅ\": sum(v.startswith(\"์
๋ ฅ\") for v in res.values()),\n",
" \"ํ๋ณ ์ ํ๋\": np.mean([res[r] == truth[r] for r in judged]) if judged else 0, \"์ฒ๋ฆฌ๋(์ฅ/s)\": int(n / dt),\n",
" \"์ฑ๋ฅ์ ํ ๋ชจ๋\": f\"์ฒญํฌ {int(np.ceil(n / CAPACITY))}ํ\" if n > CAPACITY else \"-\"})\n",
"df = pd.DataFrame(rows)\n",
"print(df.round(3).to_string(index=False))\n",
"print(f\"\\n์์คํ
๋ค์ด {df['๋ค์ด'].sum()}๊ฑด, ์ต๋ ์ ์ค๋ฅ {df['์ ์ค๋ฅ '].max():.0%} (๋ชฉํ ๋ค์ด 0๊ฑดยท์ ์ค 0%: {'๋ฌ์ฑ' if df['๋ค์ด'].sum() == 0 and df['์ ์ค๋ฅ '].max() == 0 else '๋ฏธ๋ฌ'})\")\n",
"\n",
"# ํ๋ฆผ ํ๊ณ์น: ํ๋ฆผ ์ ๋๋ฅผ ์ฌ๋ฆฌ๋ฉฐ ์ ํ๋๊ฐ 75% ์๋๋ก ๋จ์ด์ง๋ ์ง์ \n",
"print(\"\\nํ๋ฆผ(sigma) ํ๋ณ ์ ํ๋ ํ๋ณ๋ถ๊ฐ ๋น์จ\")\n",
"limit = None\n",
"for b in [0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5]:\n",
" reqs, truth = load_case(150, 50, blur=(b, b))\n",
" res = serve(reqs, {\"์ ์ฒ๋ฆฌ\": 0, \"ํํํน์ง\": 0, \"์ถ๋ก \": 0})\n",
" judged = [r for r, v in res.items() if v in CLASSES]\n",
" acc = np.mean([res[r] == truth[r] for r in judged])\n",
" if limit is None and acc < 0.75:\n",
" limit = b\n",
" print(f\"{b:>8} {acc:.3f} {1 - len(judged) / len(res):.3f}\")\n",
"print(f\"ํ๊ณ์น: ํ๋ฆผ {limit} ์ด์์์ ์ ํ๋ 75% ๋ฏธ๋ง -> ํด๋น ์์์ ํ์ต ๋ฌธํญ์์ ์ ์ธํ๊ฑฐ๋ '๊ณ ๋๋'๋ก ๋ถ๋ฅ\")\n",
"\n",
"# ๋ฉ๋ชจ๋ฆฌ ๋์ ํ์ธ: ๊ฐ์ ๋ถํ๋ฅผ 3ํ ๋ฐ๋ณตํด ๋ฉ๋ชจ๋ฆฌ ์ฆ๊ฐ๋ ์ธก์ \n",
"tracemalloc.start()\n",
"reqs, _ = load_case(300, 99)\n",
"mem = []\n",
"for _ in range(3):\n",
" serve(reqs, {\"์ ์ฒ๋ฆฌ\": 0, \"ํํํน์ง\": 0, \"์ถ๋ก \": 0}); mem.append(tracemalloc.get_traced_memory()[0] / 1e6)\n",
"peak = tracemalloc.get_traced_memory()[1] / 1e6\n",
"tracemalloc.stop()\n",
"print(f\"๋ฐ๋ณต ํ ๋ฉ๋ชจ๋ฆฌ {', '.join(f'{m:.1f}' for m in mem)} MB (์ต๋ {peak:.1f} MB), ์ฆ๊ฐ ์์: {mem[-1] - mem[0] < 1}\")\n",
"tot = sum(stage.values())\n",
"print(\"๋ณ๋ชฉ ๊ตฌ๊ฐ:\", \", \".join(f\"{k} {v / tot:.0%}\" for k, v in sorted(stage.items(), key=lambda x: -x[1])), \"-> ํํํน์ง ๊ณ์ฐ์ ๋ณ๋ ฌํํ๋ฉด ์ฒ๋ฆฌ๋์ด ๊ฐ์ฅ ํฌ๊ฒ ์ค๋ฅธ๋ค\")\n",
"\n",
"plt.figure(figsize=(8, 4))\n",
"plt.bar(range(len(df)), df[\"์ฒ๋ฆฌ๋(์ฅ/s)\"], 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))]); plt.ylabel(\"images / s\")\n",
"plt.title(\"Stress test throughput (red = queued/degraded mode)\"); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
08 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
" ์ํฉ ์์ฒญ ์๋ต ์ ์ค๋ฅ ๋ค์ด ํ๋ณ๋ถ๊ฐ ์
๋ ฅ์ค๋ฅ ํ๋ณ ์ ํ๋ ์ฒ๋ฆฌ๋(์ฅ/s) ์ฑ๋ฅ์ ํ ๋ชจ๋\n",
" ํ์ 100์ฅ 100 100 0.0 0 37 0 0.889 731 -\n",
" 300% ์ฆ๊ฐ 400์ฅ 400 400 0.0 0 155 0 0.918 706 ์ฒญํฌ 2ํ\n",
" 500% ์ฆ๊ฐ 600์ฅ 600 600 0.0 0 221 0 0.918 707 ์ฒญํฌ 3ํ\n",
" 500% + ์์ ์
๋ ฅ 3% 600 600 0.0 0 216 18 0.915 802 ์ฒญํฌ 3ํ\n",
"500% + ๊ทน์ ํ์ง(ํ๋ฆผ 3.0) 600 600 0.0 0 366 0 0.654 660 ์ฒญํฌ 3ํ\n",
"\n",
"์์คํ
๋ค์ด 0๊ฑด, ์ต๋ ์ ์ค๋ฅ 0% (๋ชฉํ ๋ค์ด 0๊ฑดยท์ ์ค 0%: ๋ฌ์ฑ)\n",
"\n",
"ํ๋ฆผ(sigma) ํ๋ณ ์ ํ๋ ํ๋ณ๋ถ๊ฐ ๋น์จ\n",
" 0.5 1.000 0.020\n",
" 1.0 0.992 0.167\n",
" 1.5 0.937 0.367\n",
" 2.0 0.825 0.467\n",
" 2.5 0.710 0.587\n",
" 3.0 0.566 0.647\n",
" 3.5 0.462 0.653\n",
"ํ๊ณ์น: ํ๋ฆผ 2.5 ์ด์์์ ์ ํ๋ 75% ๋ฏธ๋ง -> ํด๋น ์์์ ํ์ต ๋ฌธํญ์์ ์ ์ธํ๊ฑฐ๋ '๊ณ ๋๋'๋ก ๋ถ๋ฅ\n",
"๋ฐ๋ณต ํ ๋ฉ๋ชจ๋ฆฌ 0.4, 0.4, 0.4 MB (์ต๋ 30.4 MB), ์ฆ๊ฐ ์์: True\n",
"๋ณ๋ชฉ ๊ตฌ๊ฐ: ํํํน์ง 68%, ์ ์ฒ๋ฆฌ 31%, ์ถ๋ก 1% -> ํํํน์ง ๊ณ์ฐ์ ๋ณ๋ ฌํํ๋ฉด ์ฒ๋ฆฌ๋์ด ๊ฐ์ฅ ํฌ๊ฒ ์ค๋ฅธ๋ค\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
09. AAR ๊ธฐ๋ฐ ์ฅ๋ณ ๋ค๋น๋ ์ค๋ต ๋นํ์ฒด ๋ถ์ ๋ฐ ํ์ต ๋ฐ์ดํฐ์
์ฆ๊ฐ ์ฌํ์ต\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"AAR ๊ธฐ๋ฐ ์ฅ๋ณ ๋ค๋น๋ ์ค๋ต ๋นํ์ฒด ๋ถ์ ๋ฐ ํ์ต ๋ฐ์ดํฐ๋ฅผ ์ฆ๊ฐ ์ฌํ์ตํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **AAR ์ฌํ๊ฒํ ๋ก ์ฅ๋ณ์ ๋ค๋น๋ ์ค๋ต ๋นํ์ฒด์ AI ๋ถ๋ฅ๊ธฐ์ ์ค๋ฅ ์กฐ๊ฑด์ ๋ถ์ํ๊ณ , ์ทจ์ฝ ๋ฐ์ดํฐ๋ฅผ ์ฆ๊ฐํด ์ฌํ์ตํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๊ฐ์ ์ฅ๋ณ 40๋ช
์ ํ์ต ๊ธฐ๋ก์์ ๋ค๋น๋ ์ค๋ต ์์ ์ฐพ๊ณ , ๊ธฐ๋ณธ ์กฐ๊ฑด ์ค์ฌ์ผ๋ก ํ์ตํ ํํ ๋ชจ๋ธ์ ์ค๋ฅ๋ฅผ ์กฐ๊ฑดยทํด๋์ค๋ณ๋ก ๋ถ๋ฅ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `docs/trn03_aar_report.md ๋ฐ configs/trn03_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(์ฌํ๊ฒํ ) ํ์์ผ๋ก ์ฅ๋ณ ํ์ต ๊ธฐ๋ก์ ๋ค๋น๋ ์ค๋ต ๋นํ์ฒด์ AI ๋ถ๋ฅ๊ธฐ์ ์ค๋ฅ ์กฐ๊ฑด์ ์์ธ๋ณ๋ก ๋ถ์ํ๊ณ , ์ทจ์ฝ ์กฐ๊ฑดยทํด๋์ค๋ฅผ ์ฆ๊ฐํ ๋ฐ์ดํฐ๋ก ์ฌํ์ตํด ์ข
ํฉ ์ ํ๋ ํฅ์ยท์ฌ๋ฐ ์ค๋ฅยทํ๊ท ์ฌ๋ถ์ ์ฌ๋ณด์ ํ๋ผ๋ฏธํฐ๋ฅผ ์ฐ์ถํ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from matplotlib.path import Path as MplPath\n",
"from scipy import ndimage\n",
"from scipy.spatial import ConvexHull\n",
"from sklearn.neural_network import MLPClassifier\n",
"from sklearn.preprocessing import StandardScaler\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ํฉ์ฑ ์ค๋ฃจ์ฃ ์์ฑ๊ธฐ (์ค์ ๊ธฐ์ฒด ์ฌ์งยท๋๋ฉดยท์ ์ ์ฌ์ฉ ์ ํจ)\n",
"# ์ฌ๊ฐํยท์ผ๊ฐํยท์ ๊ฐ์ ๊ธฐ๋ณธ ๋ํ์ ์กฐํฉํ ์ถ์ ์ค๋ฃจ์ฃ์ 32x32 ํ๋ฐฑ ์ด๋ฏธ์ง๋ก ๊ทธ๋ฆฐ๋ค.\n",
"# ํ์ (๋ค๊ฐ๋), ์๊ทผ ์ถ์(์ธก๋ฉด ์์ ), ํฌ๊ธฐ(์๊ฑฐ๋ฆฌ), ํ๋ฆผยท์ก์ยท์ ๋๋นยท๊ตฌ๋ฆยท๊ฐ๋ฆผยท์ผ๊ฐ ๋ฐ์ ์ ์กฐ์ ํ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"IMG = 32\n",
"CLASSES = [\"civil_quad\", \"civil_fw\", \"attack_fw\", \"cruise\", \"ballistic\", \"bird\"]\n",
"KOR = {\"civil_quad\": \"๋ฏผ๊ฐ ์ฟผ๋์ฝฅํฐํ\", \"civil_fw\": \"๋ฏผ๊ฐ ๊ณ ์ ์ตํ\", \"attack_fw\": \"์ผ๊ฐ์ต ์ํญ๋๋ก ํ\",\n",
" \"cruise\": \"์ํญ๋ฏธ์ฌ์ผํ\", \"ballistic\": \"ํ๋๋ฏธ์ฌ์ผํ\", \"bird\": \"์กฐ๋ฅ\"}\n",
"THREAT = {\"attack_fw\", \"cruise\", \"ballistic\"}\n",
"\n",
"\n",
"def _rect(x0, x1, y0, y1):\n",
" return [(x0, y0), (x1, y0), (x1, y1), (x0, y1)]\n",
"\n",
"\n",
"def _bar(p, q, w):\n",
" (x0, y0), (x1, y1) = p, q\n",
" n = np.array([y0 - y1, x1 - x0]) / np.hypot(x1 - x0, y1 - y0) * w / 2\n",
" return [(x0 + n[0], y0 + n[1]), (x1 + n[0], y1 + n[1]), (x1 - n[0], y1 - n[1]), (x0 - n[0], y0 - n[1])]\n",
"\n",
"\n",
"# ๋ํ ์ ์(๋จ์ ์ขํ, ๊ธฐ์ ๋ฐฉํฅ = +y). ๋ชจ๋ ์ถ์ ๋ํ์ด๋ฉฐ ํน์ ๊ธฐ์ข
์ ๋ณธ๋จ์ง ์๋๋ค.\n",
"SHAPES = {\n",
" \"civil_quad\": {\"poly\": [_bar((-.55, -.55), (.55, .55), .1), _bar((-.55, .55), (.55, -.55), .1), _rect(-.15, .15, -.15, .15)],\n",
" \"circ\": [(-.55, -.55, .24), (.55, .55, .24), (-.55, .55, .24), (.55, -.55, .24)]},\n",
" \"civil_fw\": {\"poly\": [_rect(-.07, .07, -.8, .72), _rect(-.9, .9, .05, .25), _rect(-.33, .33, -.8, -.66)], \"circ\": []},\n",
" \"attack_fw\": {\"poly\": [[(0, .85), (.85, -.5), (.12, -.42), (0, -.6), (-.12, -.42), (-.85, -.5)]], \"circ\": []},\n",
" \"cruise\": {\"poly\": [_rect(-.07, .07, -.9, .7), [(-.07, .7), (0, .92), (.07, .7)], _rect(-.42, .42, -.05, .07),\n",
" [(-.07, -.9), (-.24, -.95), (-.07, -.74)], [(.07, -.9), (.24, -.95), (.07, -.74)]], \"circ\": []},\n",
" \"ballistic\": {\"poly\": [_rect(-.13, .13, -.85, .6), [(-.13, .6), (0, .95), (.13, .6)],\n",
" [(-.13, -.85), (-.34, -.95), (-.13, -.58)], [(.13, -.85), (.34, -.95), (.13, -.58)]], \"circ\": []},\n",
" \"bird\": {\"poly\": [[(-.9, .12), (-.5, .28), (-.12, .06), (0, .22), (.12, .06), (.5, .28), (.9, .12), (.5, .12),\n",
" (.12, -.1), (0, -.38), (-.12, -.1), (-.5, .12)]], \"circ\": []},\n",
"}\n",
"_S = 2\n",
"_g = (np.arange(IMG * _S) + 0.5) / _S\n",
"_GX, _GY = np.meshgrid(_g, _g)\n",
"_PTS = np.c_[_GX.ravel(), _GY.ravel()]\n",
"_PATHS = {c: [MplPath(np.array(p)) for p in s[\"poly\"]] for c, s in SHAPES.items()}\n",
"\n",
"\n",
"def draw(cls, rng, angle=None, scale=None, fore=None, blur=0.3, noise=4.0, contrast=1.0,\n",
" clutter=0.0, occlude=0.0, invert=False, shift=1.5):\n",
" \"\"\"ํด๋์ค 1๊ฐ ์ค๋ฃจ์ฃ ์ด๋ฏธ์ง(uint8 32x32)๋ฅผ ๊ทธ๋ฆฐ๋ค. ๋ฐฐ๊ฒฝ์ ๋ฐ์ ํ๋, ๋นํ์ฒด๋ ์ด๋์ด ๊ทธ๋ฆผ์.\"\"\"\n",
" angle = rng.uniform(0, 360) if angle is None else angle\n",
" scale = rng.uniform(.7, .95) if scale is None else scale\n",
" fore = rng.uniform(.85, 1.0) if fore is None else fore\n",
" th = np.radians(angle)\n",
" c0 = IMG / 2 + rng.uniform(-shift, shift, 2)\n",
" d = (_PTS - c0) / (scale * IMG / 2) # ์ด๋ฏธ์ง ์ขํ -> ๋ํ ์ขํ(์ญ๋ณํ)\n",
" u = d[:, 0] * np.cos(th) + d[:, 1] * np.sin(th)\n",
" v = (-d[:, 0] * np.sin(th) + d[:, 1] * np.cos(th)) / fore # ์๊ทผ ์ถ์(๊ธฐ์ ๋ฐฉํฅ์ผ๋ก ์งง์ ๋ณด์)\n",
" uv = np.c_[u, -v]\n",
" m = np.zeros(len(uv), bool)\n",
" near = np.flatnonzero(np.abs(uv).max(axis=1) <= 1.0) # ๋ํ์ด ์์ ์ ์๋ ์์ญ๋ง ๊ฒ์ฌ(์๋)\n",
" for p in _PATHS[cls]:\n",
" m[near] |= p.contains_points(uv[near])\n",
" for cx, cy, r in SHAPES[cls][\"circ\"]:\n",
" m |= (uv[:, 0] - cx) ** 2 + (uv[:, 1] - cy) ** 2 <= r * r\n",
" cov = m.reshape(IMG, _S, IMG, _S).mean(axis=(1, 3))\n",
" if occlude > 0: # ๋ถ๋ถ ๊ฐ๋ฆผ(๊ตฌ๋ฆ ์กฐ๊ฐ)\n",
" h = int(IMG * np.sqrt(occlude)); y0, x0 = rng.integers(8, IMG - 8 - h // 2, 2)\n",
" cov[y0:y0 + h, x0:x0 + h] = 0\n",
" bg = rng.uniform(180, 220) + np.linspace(-10, 10, IMG)[:, None]\n",
" fg = rng.uniform(30, 60)\n",
" img = bg * (1 - cov) + fg * cov\n",
" if clutter > 0: # ๊ตฌ๋ฆยท๋ฐฐ๊ฒฝ ์ผ๋ฃฉ\n",
" img -= clutter * 90 * np.clip(ndimage.gaussian_filter(rng.normal(0, 1, (IMG, IMG)), 3) * 4, 0, None)\n",
" if blur > 0:\n",
" img = ndimage.gaussian_filter(img, blur)\n",
" mean = img.mean()\n",
" img = mean + (img - mean) * contrast\n",
" img = img + rng.normal(0, noise, img.shape)\n",
" if invert: # ์ผ๊ฐ ์ด์(IR) ๊ทน์ฑ ๋ฐ์ \n",
" img = 255 - img\n",
" return np.clip(img, 0, 255).astype(np.uint8)\n",
"\n",
"\n",
"# ์ดฌ์ ์กฐ๊ฑด ํ๋ฆฌ์
: ๊ธฐ๋ณธ + ๋ณตํฉ ์ํ ์๋๋ฆฌ์ค\n",
"CONDITIONS = {\n",
" \"basic\": {},\n",
" \"offcenter\": {\"shift\": 5.0, \"fore\": (.6, .85)},\n",
" \"far\": {\"scale\": (.32, .45), \"blur\": .6},\n",
" \"blur\": {\"blur\": (1.2, 1.8)},\n",
" \"lowcon\": {\"contrast\": (.2, .35)},\n",
" \"clutter\": {\"clutter\": .8},\n",
" \"occlude\": {\"occlude\": .08},\n",
" \"oblique\": {\"fore\": (.35, .6)},\n",
" \"night\": {\"invert\": True, \"noise\": 10.0},\n",
" \"noise\": {\"noise\": 28.0},\n",
" \"combo\": {\"scale\": (.4, .55), \"blur\": (.8, 1.1), \"noise\": 12.0, \"contrast\": (.6, .8)},\n",
"}\n",
"\n",
"\n",
"def draw_cond(cls, cond, rng):\n",
" kw = {}\n",
" for k, v in CONDITIONS[cond].items():\n",
" kw[k] = rng.uniform(*v) if isinstance(v, tuple) else v\n",
" return draw(cls, rng, **kw)\n",
"\n",
"\n",
"def make_dataset(n_per_class, seed, conds=(\"basic\",)):\n",
" \"\"\"(์ด๋ฏธ์ง ๋ฐฐ์ด N x 32 x 32, ์ ๋ต ๋ผ๋ฒจ, ์กฐ๊ฑด) ๋ฐํ. ์กฐ๊ฑด์ conds์์ ๋ฒ๊ฐ์ ๊ณ ๋ฅธ๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" X, y, c = [], [], []\n",
" for cls in CLASSES:\n",
" for i in range(n_per_class):\n",
" cond = conds[i % len(conds)]\n",
" X.append(draw_cond(cls, cond, rng)); y.append(cls); c.append(cond)\n",
" return np.array(X), np.array(y), np.array(c)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์ ์ฒ๋ฆฌ(๋ฐฐ์น ๋ฒกํฐํ)ยทํํ ํน์ง ๊ณตํต ํจ์ ----\n",
"OUT = 24 # ํฌ๊ธฐ ์ ๊ทํ ๊ฒฐ๊ณผ(24x24)\n",
"NOISE_T = 12.0 # ์ก์ ์ถ์ ๊ฐ์ด ์ด๋ณด๋ค ํฌ๋ฉด 3x3 ์ค๊ฐ๊ฐ ํํฐ ์ ์ฉ(์
์ฒํยท์ผ์ ์ก์)\n",
"FEATS = [\"elong\", \"fill\", \"solidity\", \"n_parts\", \"end_gap\", \"mid_bulge\", \"w_max\", \"compact\"]\n",
"\n",
"\n",
"def preprocess(images, noise_t=NOISE_T):\n",
" \"\"\"N์ฅ ์ผ๊ด ์ฒ๋ฆฌ: ๊ฒฐ์ธก ๋ณด์ -> ๊ทน์ฑ(์ผ๊ฐ ๋ฐ์ ) ๋ณด์ -> ์กฐ๊ฑด๋ณ ์ก์ ์ ๊ฑฐ -> ๋ช
์ ๋๋น ๊ฐํ -> ๋ถํ -> ์ค๊ณฝ์ -> ๋ฐฉํฅยทํฌ๊ธฐ ์ ๊ทํ.\"\"\"\n",
" f = np.array(images, dtype=float).reshape(-1, IMG, IMG)\n",
" n = len(f)\n",
" bad = ~np.isfinite(f)\n",
" missing = bad.sum(axis=(1, 2))\n",
" if bad.any(): # ๊ฒฐ์ธก ํฝ์
: ์์ ์ค์๊ฐ์ผ๋ก ์ฑ์ด ๋ค ์ฃผ๋ณ 3x3 ์ค๊ฐ๊ฐ์ผ๋ก ๋ณด์ \n",
" med = np.array([np.median(x[np.isfinite(x)]) if np.isfinite(x).any() else 128.0 for x in f])\n",
" f = np.where(bad, med[:, None, None], f)\n",
" f = np.where(bad, ndimage.median_filter(f, size=(1, 3, 3)), f)\n",
" border = np.concatenate([f[:, 0], f[:, -1], f[:, :, 0], f[:, :, -1]], axis=1)\n",
" inverted = np.median(border, axis=1) < 128 # ๋ฐฐ๊ฒฝ์ด ์ด๋์ฐ๋ฉด ์ผ๊ฐ ์ด์์ผ๋ก ๋ณด๊ณ ๋ฐ์ \n",
" f[inverted] = 255 - f[inverted]\n",
" med3 = ndimage.median_filter(f, size=(1, 3, 3))\n",
" noise = 1.4826 * np.median(np.abs(f - med3).reshape(n, -1), axis=1)\n",
" noisy = noise > noise_t\n",
" f[noisy] = med3[noisy]\n",
" lo, hi = np.percentile(f.reshape(n, -1), [1, 99], axis=1)\n",
" e = np.clip((f - lo[:, None, None]) / np.maximum(hi - lo, 1e-6)[:, None, None], 0, 1) # ๋ช
์ ๋๋น ๊ฐํ\n",
" mask = e < _otsu(e)[:, None, None]\n",
" mask &= ndimage.uniform_filter(mask.astype(float), size=(1, 3, 3)) * 9 >= 2.5 # ๊ณ ๋ฆฝ ์ ์ก์ ์ ๊ฑฐ\n",
" edge = np.hypot(ndimage.sobel(e, axis=1), ndimage.sobel(e, axis=2)) # ์ค๊ณฝ์ (Sobel)\n",
" ok = (mask.sum(axis=(1, 2)) >= 4) & (hi - lo > 8)\n",
" return {\"enh\": e, \"mask\": mask, \"edge\": edge / np.maximum(edge.max(axis=(1, 2)), 1e-6)[:, None, None],\n",
" \"norm\": _normalize(1 - e, mask, ok), \"inverted\": inverted, \"noise\": noise, \"denoised\": noisy,\n",
" \"missing\": missing, \"contrast\": hi - lo, \"ok\": ok}\n",
"\n",
"\n",
"def _otsu(e, bins=64):\n",
" n = len(e)\n",
" idx = np.minimum((e * bins).astype(int), bins - 1) + bins * np.arange(n)[:, None, None]\n",
" hist = np.bincount(idx.ravel(), minlength=bins * n).reshape(n, bins).astype(float)\n",
" centers = (np.arange(bins) + 0.5) / bins\n",
" w = np.cumsum(hist, axis=1); m = np.cumsum(hist * centers, axis=1)\n",
" tot, mt = w[:, -1:], m[:, -1:]\n",
" between = (mt * w / tot - m) ** 2 / np.maximum(w * (tot - w), 1)\n",
" return (np.argmax(between, axis=1) + 1) / bins\n",
"\n",
"\n",
"def _normalize(obj, mask, ok):\n",
" \"\"\"์ฃผ์ถ ๋ฐฉํฅ์ ์ธ๋ก๋ก ๋๋ฆฌ๊ณ ๋นํ์ฒด ์์ญ๋ง 24x24๋ก ๋ค์ ์ํ๋งํ๋ค(ํ์ ยทํฌ๊ธฐยท์์น ๋ถ๋ณ). ํ ๋ฒ์ ๋ณด๊ฐ์ผ๋ก ์ผ๊ด ์ฒ๋ฆฌ.\"\"\"\n",
" n = len(obj)\n",
" w = mask.astype(float)\n",
" m00 = np.maximum(w.sum(axis=(1, 2)), 1)\n",
" yy, xx = np.mgrid[0:IMG, 0:IMG].astype(float)\n",
" cy, cx = (w * yy).sum(axis=(1, 2)) / m00, (w * xx).sum(axis=(1, 2)) / m00\n",
" dy, dx = yy - cy[:, None, None], xx - cx[:, None, None]\n",
" mu20, mu02, mu11 = (w * dy * dy).sum(axis=(1, 2)), (w * dx * dx).sum(axis=(1, 2)), (w * dy * dx).sum(axis=(1, 2))\n",
" th = 0.5 * np.arctan2(2 * mu11, mu20 - mu02)\n",
" a = np.stack([np.cos(th), np.sin(th)], 1) # ์ฃผ์ถ(y, x)\n",
" b = np.stack([-np.sin(th), np.cos(th)], 1) # ๋ถ์ถ\n",
" pa = np.where(mask, dy * a[:, 0, None, None] + dx * a[:, 1, None, None], 0)\n",
" pb = np.where(mask, dy * b[:, 0, None, None] + dx * b[:, 1, None, None], 0)\n",
" half = np.maximum(np.abs(pa).max(axis=(1, 2)), np.abs(pb).max(axis=(1, 2))) + 1.0\n",
" g = np.linspace(-1, 1, OUT)\n",
" U, V = np.meshgrid(g, g, indexing=\"ij\")\n",
" Y = cy[:, None, None] + half[:, None, None] * (U * a[:, 0, None, None] + V * b[:, 0, None, None])\n",
" X = cx[:, None, None] + half[:, None, None] * (U * a[:, 1, None, None] + V * b[:, 1, None, None])\n",
" N = np.broadcast_to(np.arange(n)[:, None, None], Y.shape)\n",
" out = ndimage.map_coordinates(obj * mask, [N, Y, X], order=1, cval=0.0)\n",
" skew = (out * (U ** 3)).sum(axis=(1, 2))\n",
" out[skew < 0] = out[skew < 0, ::-1] # ๋ฌด๊ฒ๊ฐ ์ ๋ฆฐ ์ชฝ์ ์๋ก(180๋ ๋ชจํธ์ฑ ์ ๊ฑฐ)\n",
" out[~ok] = 0\n",
" return np.clip(out, 0, 1)\n",
"\n",
"\n",
"def shape_features(mask):\n",
" \"\"\"๋ถํ ๋ง์คํฌ 1์ฅ์์ ํ์ ์ ๋ฌด๊ดํ ํํ ํน์ง 8์ข
์ ๊ณ์ฐํ๋ค.\"\"\"\n",
" if mask.sum() < 4:\n",
" return dict.fromkeys(FEATS, 0.0)\n",
" yx = np.argwhere(mask).astype(float)\n",
" c = yx - yx.mean(axis=0)\n",
" val, vec = np.linalg.eigh(np.cov(c.T) + 1e-6 * np.eye(2))\n",
" t, s = c @ vec[:, 1], c @ vec[:, 0]\n",
" length, width = np.ptp(t) + 1, np.ptp(s) + 1\n",
" corners = np.vstack([yx + d for d in ([.5, .5], [.5, -.5], [-.5, .5], [-.5, -.5])])\n",
" try:\n",
" hull = ConvexHull(corners).volume\n",
" except Exception:\n",
" hull = float(mask.sum())\n",
" bins = np.digitize(t, np.linspace(t.min(), t.max() + 1e-6, 4)[1:-1])\n",
" w = np.array([np.ptp(s[bins == k]) + 1 if (bins == k).any() else 0 for k in range(3)]) / length\n",
" boundary = mask & ~ndimage.binary_erosion(mask)\n",
" return {\"elong\": float(np.sqrt(val[1] / max(val[0], 1e-6))), \"fill\": float(mask.sum() / (length * width)),\n",
" \"solidity\": float(mask.sum() / max(hull, 1)), \"n_parts\": float(ndimage.label(mask)[1]),\n",
" \"end_gap\": float(abs(w[0] - w[2])), \"mid_bulge\": float(w[1] - max(w[0], w[2])),\n",
" \"w_max\": float(w.max()), \"compact\": float(boundary.sum() / np.sqrt(mask.sum()))}\n",
"\n",
"\n",
"def feature_matrix(images):\n",
" \"\"\"์ ์ฒ๋ฆฌ ๊ฒฐ๊ณผ, ํํ ํน์ง ํ๋ ฌ(N x 8), ์ ๊ทํ ์ด๋ฏธ์ง ํ๋ ฌ(N x 576)์ ๋๋ ค์ค๋ค.\"\"\"\n",
" P = preprocess(images)\n",
" F = np.array([[d[k] for k in FEATS] for d in map(shape_features, P[\"mask\"])])\n",
" return P, F, P[\"norm\"].reshape(len(F), -1)\n",
"\n",
"# ---- ๋ถ๋ฅ ๋ชจ๋ธ ๊ณตํต ํจ์(๋ค์ธต ํผ์
ํธ๋ก : ์ ๊ทํ ์ด๋ฏธ์ง 576ํฝ์
+ ํํ ํน์ง 8์ข
) ----\n",
"def to_input(F, N, scaler):\n",
" return np.c_[N, scaler.transform(F) * 0.3]\n",
"\n",
"\n",
"def train_model(n_per_class=150, seed=5, conds=tuple(CONDITIONS), hidden=(64,)):\n",
" X, y, _ = make_dataset(n_per_class, seed, conds)\n",
" P, F, N = feature_matrix(X)\n",
" scaler = StandardScaler().fit(F)\n",
" model = MLPClassifier(hidden, max_iter=200, early_stopping=True, random_state=0)\n",
" model.fit(to_input(F, N, scaler), [CLASSES.index(v) for v in y])\n",
" return model, scaler\n",
"\n",
"\n",
"def predict(model, scaler, images):\n",
" \"\"\"(์ ์ฒ๋ฆฌ ๊ฒฐ๊ณผ, ํด๋์ค๋ณ ํ๋ฅ N x 6)์ ๋๋ ค์ค๋ค.\"\"\"\n",
" P, F, N = feature_matrix(images)\n",
" return P, model.predict_proba(to_input(F, N, scaler))\n",
"\n",
"\n",
"def fit(X, y):\n",
" P, F, N = feature_matrix(X)\n",
" sc = StandardScaler().fit(F)\n",
" m = MLPClassifier((96,), max_iter=200, early_stopping=True, random_state=0).fit(to_input(F, N, sc), [CLASSES.index(v) for v in y])\n",
" return m, sc\n",
"\n",
"\n",
"def evaluate(m, sc, X):\n",
" return np.array(CLASSES)[predict(m, sc, X)[1].argmax(1)]\n",
"\n",
"\n",
"# [1. ๋ฌด์์ด ์ผ์ด๋ฌ๋] ์ฅ๋ณ ํ์ต ๊ธฐ๋ก(๊ฐ์ 40๋ช
x 30๋ฌธํญ) ๋ค๋น๋ ์ค๋ต\n",
"rng = np.random.default_rng(3)\n",
"CONF = {\"civil_fw\": (\"attack_fw\", .30), \"attack_fw\": (\"civil_fw\", .25), \"cruise\": (\"ballistic\", .40), \"ballistic\": (\"cruise\", .35),\n",
" \"bird\": (\"civil_fw\", .15), \"civil_quad\": (\"civil_fw\", .05)}\n",
"log = []\n",
"for s in range(40):\n",
" for q in range(30):\n",
" c = CLASSES[rng.integers(6)]\n",
" wrong, p = CONF[c]\n",
" log.append({\"์ฅ๋ณ\": f\"L{s + 1:02d}\", \"์ ๋ต\": c, \"์๋ต\": wrong if rng.random() < p else c})\n",
"lg = pd.DataFrame(log)\n",
"err = lg[lg[\"์ ๋ต\"] != lg[\"์๋ต\"]]\n",
"print(f\"[1. ๋ฌด์์ด ์ผ์ด๋ฌ๋] ์๋ต {len(lg)}๊ฑด ์ค ์ค๋ต {len(err)}๊ฑด({len(err) / len(lg):.1%})\")\n",
"print(\"๋ค๋น๋ ์ค๋ต ์:\", (err[\"์ ๋ต\"] + \"->\" + err[\"์๋ต\"]).value_counts().head(3).to_dict())\n",
"\n",
"# [2. ์ ์ผ์ด๋ฌ๋] AI ๋ถ๋ฅ๊ธฐ(ํํ: ๊ธฐ๋ณธยทํ์ ยท์ ๋๋นยท์ผ๊ฐ ์กฐ๊ฑด ์ค์ฌ ํ์ต) ์ค๋ฅ๋ฅผ ์กฐ๊ฑด๋ณ๋ก ๋ถ๋ฅ\n",
"base_conds = (\"basic\", \"offcenter\", \"lowcon\", \"night\")\n",
"Xtr, ytr, _ = make_dataset(150, 5, base_conds)\n",
"m0, s0 = fit(Xtr, ytr)\n",
"Xev, yev, cev = make_dataset(110, 31, tuple(CONDITIONS)) # AAR ํ๊ฐ ์ธํธ(11๊ฐ ์กฐ๊ฑด)\n",
"p0 = evaluate(m0, s0, Xev)\n",
"acc0 = (p0 == yev).mean()\n",
"by = pd.Series(p0 != yev).groupby(cev).mean().sort_values(ascending=False)\n",
"print(f\"\\n[2. ์ ์ผ์ด๋ฌ๋] ํํ ๋ชจ๋ธ ์ข
ํฉ ์ ํ๋ {acc0:.3f}, ์ค๋ฅ์จ ์์ ์กฐ๊ฑด:\", by.head(4).round(2).to_dict())\n",
"weak_cls = pd.Series(p0 != yev).groupby(yev).mean().sort_values(ascending=False)\n",
"print(\"์ค๋ฅ์จ ์์ ํด๋์ค:\", weak_cls.head(3).round(2).to_dict(), \"-> ์์ธ: ํ์ต์ ์๊ฑฐ๋ฆฌยทํ๋ฆผยท๊ฐ๋ฆผยท์ธก๋ฉด ์์ ์์์ด ์์\")\n",
"\n",
"# [3. ๋ฌด์์ ๋ฐ๊พธ๋] ์ทจ์ฝ ์กฐ๊ฑด ์ฆ๊ฐ + ๋ค๋น๋ ์ค๋ต ํด๋์ค ๊ฐ์ค ์ฆ๊ฐ ํ ์ฌํ์ต\n",
"weak_conds = tuple(by.index[by > 0.15]) or tuple(CONDITIONS)\n",
"Xa, ya, _ = make_dataset(110, 6, weak_conds)\n",
"extra = [c for c in (\"cruise\", \"ballistic\", \"civil_fw\", \"attack_fw\")]\n",
"rng2 = np.random.default_rng(7)\n",
"Xb = np.array([draw_cond(c, weak_conds[i % len(weak_conds)], rng2) for i, c in enumerate(extra * 40)])\n",
"yb = np.array(extra * 40)\n",
"m1, s1 = fit(np.r_[Xtr, Xa, Xb], np.r_[ytr, ya, yb])\n",
"prob1 = predict(m1, s1, Xev)[1]\n",
"p1 = np.array(CLASSES)[prob1.argmax(1)]\n",
"acc1 = (p1 == yev).mean()\n",
"print(f\"\\n[3. ๋ฌด์์ ๋ฐ๊พธ๋] ์ฆ๊ฐ ์กฐ๊ฑด {list(weak_conds)}, ํผ๋ ํด๋์ค 4์ข
๊ฐ 40์ฅ ์ถ๊ฐ\")\n",
"print(f\"์ข
ํฉ ์ ํ๋ {acc0:.3f} -> {acc1:.3f} ({(acc1 - acc0) * 100:+.1f}%p, ๋ชฉํ +10%p: {'๋ฌ์ฑ' if acc1 - acc0 >= 0.10 else '๋ฏธ๋ฌ'})\")\n",
"\n",
"# ํ๊ท ํ
์คํธ: AAR ๋ฑ๋ก ์ค๋ฅ์ ์ฌ๋ฐ, ๊ธฐ์กด ์ ๋ต์ ํ๊ท\n",
"reg = p0 != yev\n",
"again = reg & (p1 != yev)\n",
"print(f\"AAR ๋ฑ๋ก ์ค๋ฅ {reg.sum()}๊ฑด ์ค ์ฌ๋ฐ {again.sum()}๊ฑด(๋ชฉํ 0๊ฑด: {'๋ฌ์ฑ' if again.sum() == 0 else '๋ฏธ๋ฌ'}), ๊ธฐ์กด ์ ๋ต ์ค ์๋ก ํ๋ฆฐ ํ๊ท {(~reg & (p1 != yev)).sum()}๊ฑด\")\n",
"print(f\"์ฌ๋ฐ ์ค๋ฅ ์ค ์ ๋ขฐ๋ 0.5 ๋ฏธ๋ง์ด๋ผ '๊ต๊ด ํ์ธ'์ผ๋ก ๊ฑธ๋ฌ์ง๋ ๊ฑด {(again & (prob1.max(1) < 0.5)).sum()}๊ฑด, ๋จ์ ์ค๋ต์ผ๋ก ๋จ๋ ๊ฑด {(again & (prob1.max(1) >= 0.5)).sum()}๊ฑด\")\n",
"comp = pd.DataFrame({\"์ฌ๋ณด์ ์ \": pd.Series(p0 == yev).groupby(cev).mean(), \"์ฌ๋ณด์ ํ\": pd.Series(p1 == yev).groupby(cev).mean()}).round(3)\n",
"print(comp.T.to_string())\n",
"print(\"๋จ์ ์ฌ๋ฐ ์ค๋ฅ๋ ๊ฒฐํฉ(combo)ยท๊ตฌ๋ฆ ์กฐ๊ฑด์ ๋ชฐ๋ ค ์์ด ๊ต๊ด ํ์ธ ๋์์ผ๋ก ๋๊ณ , ๋ค์ ์ฃผ๊ธฐ์ ์ง์ ํ์ต์ผ๋ก ๋ณด์ํ๋ค.\")\n",
"\n",
"tuned = (f\"augment_conditions: {list(weak_conds)}\\naugment_extra_per_class: {{cruise: 40, ballistic: 40, civil_fw: 40, attack_fw: 40}}\\n\"\n",
" f\"min_confidence: 0.5\\nquiz_mix: {{confusing_pairs: 0.4, weak_conditions: 0.3, random: 0.3}}\\nhidden_layer: 96\")\n",
"print(\"\\n[์ฌ๋ณด์ ํ๋ผ๋ฏธํฐ(trn03_tuned_params.yaml ๋ด์ฉ)]\\n\" + tuned)\n",
"\n",
"plt.figure(figsize=(9, 4))\n",
"x = np.arange(len(comp))\n",
"plt.bar(x - 0.2, comp[\"์ฌ๋ณด์ ์ \"], 0.4, label=\"before AAR\"); plt.bar(x + 0.2, comp[\"์ฌ๋ณด์ ํ\"], 0.4, label=\"after augmentation\")\n",
"plt.xticks(x, comp.index, rotation=30); plt.ylabel(\"accuracy\"); plt.legend(); plt.title(\"AAR retraining effect by condition (synthetic)\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
09 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[1. ๋ฌด์์ด ์ผ์ด๋ฌ๋] ์๋ต 1200๊ฑด ์ค ์ค๋ต 337๊ฑด(28.1%)\n",
"๋ค๋น๋ ์ค๋ต ์: {'cruise->ballistic': 90, 'civil_fw->attack_fw': 77, 'attack_fw->civil_fw': 66}\n",
"\n",
"[2. ์ ์ผ์ด๋ฌ๋] ํํ ๋ชจ๋ธ ์ข
ํฉ ์ ํ๋ 0.770, ์ค๋ฅ์จ ์์ ์กฐ๊ฑด: {'combo': 0.67, 'oblique': 0.62, 'occlude': 0.42, 'clutter': 0.38}\n",
"์ค๋ฅ์จ ์์ ํด๋์ค: {'cruise': 0.34, 'bird': 0.3, 'civil_fw': 0.27} -> ์์ธ: ํ์ต์ ์๊ฑฐ๋ฆฌยทํ๋ฆผยท๊ฐ๋ฆผยท์ธก๋ฉด ์์ ์์์ด ์์\n",
"\n",
"[3. ๋ฌด์์ ๋ฐ๊พธ๋] ์ฆ๊ฐ ์กฐ๊ฑด ['combo', 'oblique', 'occlude', 'clutter', 'far'], ํผ๋ ํด๋์ค 4์ข
๊ฐ 40์ฅ ์ถ๊ฐ\n",
"์ข
ํฉ ์ ํ๋ 0.770 -> 0.873 (+10.3%p, ๋ชฉํ +10%p: ๋ฌ์ฑ)\n",
"AAR ๋ฑ๋ก ์ค๋ฅ 152๊ฑด ์ค ์ฌ๋ฐ 67๊ฑด(๋ชฉํ 0๊ฑด: ๋ฏธ๋ฌ), ๊ธฐ์กด ์ ๋ต ์ค ์๋ก ํ๋ฆฐ ํ๊ท 17๊ฑด\n",
"์ฌ๋ฐ ์ค๋ฅ ์ค ์ ๋ขฐ๋ 0.5 ๋ฏธ๋ง์ด๋ผ '๊ต๊ด ํ์ธ'์ผ๋ก ๊ฑธ๋ฌ์ง๋ ๊ฑด 36๊ฑด, ๋จ์ ์ค๋ต์ผ๋ก ๋จ๋ ๊ฑด 31๊ฑด\n",
" basic blur clutter combo far lowcon night noise oblique occlude offcenter\n",
"์ฌ๋ณด์ ์ 1.0 0.933 0.617 0.333 0.683 1.0 0.983 0.967 0.383 0.583 0.983\n",
"์ฌ๋ณด์ ํ 1.0 0.967 0.650 0.450 0.867 1.0 1.000 0.950 0.967 0.750 1.000\n",
"๋จ์ ์ฌ๋ฐ ์ค๋ฅ๋ ๊ฒฐํฉ(combo)ยท๊ตฌ๋ฆ ์กฐ๊ฑด์ ๋ชฐ๋ ค ์์ด ๊ต๊ด ํ์ธ ๋์์ผ๋ก ๋๊ณ , ๋ค์ ์ฃผ๊ธฐ์ ์ง์ ํ์ต์ผ๋ก ๋ณด์ํ๋ค.\n",
"\n",
"[์ฌ๋ณด์ ํ๋ผ๋ฏธํฐ(trn03_tuned_params.yaml ๋ด์ฉ)]\n",
"augment_conditions: ['combo', 'oblique', 'occlude', 'clutter', 'far']\n",
"augment_extra_per_class: {cruise: 40, ballistic: 40, civil_fw: 40, attack_fw: 40}\n",
"min_confidence: 0.5\n",
"quiz_mix: {confusing_pairs: 0.4, weak_conditions: 0.3, random: 0.3}\n",
"hidden_layer: 96\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
10. ๋๋ก ยท๋ฏธ์ฌ์ผ ์ค๋ฃจ์ฃ ๋ถ๋ฅ ํ์ต์ฑ ์ต์ข
ํจํค์ง ๋ฐ ๋ถ๋ ๊ต์ก ๋ฐฐํฌ ๊ฐ์ด๋ ์์ฑ\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๋๋ก ยท๋ฏธ์ฌ์ผ ์ค๋ฃจ์ฃ ๋ถ๋ฅ ํ์ต์ฑ ์ต์ข
ํจํค์ง ๋ฐ ๋ถ๋ ๊ต์ก ๋ฐฐํฌ ๊ฐ์ด๋๋ฅผ ์์ฑํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ค๋ฃจ์ฃ ์์ฑ๋ถํฐ ์ ์ฒ๋ฆฌ, ๋ถ๋ฅ, ์ ๋ขฐ๋ ๋ฑ๊ธ, ํด์ฆ ์ด์, ๊ต๊ด ํด์ค ๊ฒํ ๊น์ง ํ์ต์ฑ ์ ์ฒด๋ฅผ ํตํฉํ๊ณ ๋ถ๋ ๊ต์ก ๋ฐฐํฌ ๊ฐ์ด๋๋ฅผ ์์ฑํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **AAR ์ฌ๋ณด์ ๊ฐ์ ์ ์ฉํ ํตํฉ ์ฝ๋๋ฅผ ํ ๋ฒ์ ์คํํด ์์ ๋ฌธํญ์ด ์๋ ์ฐจ๋จ๋๋์ง ํ์ธํ๊ณ ์ ํจ์ฑ์ ์ ๊ฒํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/main_trn03.py, README.md, docs/trn03_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": [
"\"\"\"ํฉ์ฑ ์ค๋ฃจ์ฃ ์์ฑยท๊ฒ์ฆ๋ถํฐ ์ ์ฒ๋ฆฌ, ๋ถ๋ฅ(AAR ์ฌ๋ณด์ ๊ฐ ์ ์ฉ), ์ ๋ขฐ๋ ๋ฑ๊ธ, ํ์ต ํด์ฆ ์ด์, ์ค๋ต ํด์ค ๊ต๊ด ๊ฒํ ๋๊ธฐ์ด๊น์ง ํ์ต์ฑ ์ ์ฒด๋ฅผ ํ ํ์ผ๋ก ํตํฉ ์คํํ๊ณ , ์์ฒด ์ ๊ฒยทํจ์ ์ปค๋ฒ๋ฆฌ์งยท๋ถ๋ ๊ต์ก ๋ฐฐํฌ ๋ธ๋ฆฌํ ์์ฝ์ ์ถ๋ ฅํ๋ค.\"\"\"\n",
"import hashlib\n",
"import json\n",
"import sys\n",
"import time\n",
"import types\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from matplotlib.path import Path as MplPath\n",
"from scipy import ndimage\n",
"from scipy.spatial import ConvexHull\n",
"from sklearn.neural_network import MLPClassifier\n",
"from sklearn.preprocessing import StandardScaler\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ํฉ์ฑ ์ค๋ฃจ์ฃ ์์ฑ๊ธฐ (์ค์ ๊ธฐ์ฒด ์ฌ์งยท๋๋ฉดยท์ ์ ์ฌ์ฉ ์ ํจ)\n",
"# ์ฌ๊ฐํยท์ผ๊ฐํยท์ ๊ฐ์ ๊ธฐ๋ณธ ๋ํ์ ์กฐํฉํ ์ถ์ ์ค๋ฃจ์ฃ์ 32x32 ํ๋ฐฑ ์ด๋ฏธ์ง๋ก ๊ทธ๋ฆฐ๋ค.\n",
"# ํ์ (๋ค๊ฐ๋), ์๊ทผ ์ถ์(์ธก๋ฉด ์์ ), ํฌ๊ธฐ(์๊ฑฐ๋ฆฌ), ํ๋ฆผยท์ก์ยท์ ๋๋นยท๊ตฌ๋ฆยท๊ฐ๋ฆผยท์ผ๊ฐ ๋ฐ์ ์ ์กฐ์ ํ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"IMG = 32\n",
"CLASSES = [\"civil_quad\", \"civil_fw\", \"attack_fw\", \"cruise\", \"ballistic\", \"bird\"]\n",
"KOR = {\"civil_quad\": \"๋ฏผ๊ฐ ์ฟผ๋์ฝฅํฐํ\", \"civil_fw\": \"๋ฏผ๊ฐ ๊ณ ์ ์ตํ\", \"attack_fw\": \"์ผ๊ฐ์ต ์ํญ๋๋ก ํ\",\n",
" \"cruise\": \"์ํญ๋ฏธ์ฌ์ผํ\", \"ballistic\": \"ํ๋๋ฏธ์ฌ์ผํ\", \"bird\": \"์กฐ๋ฅ\"}\n",
"THREAT = {\"attack_fw\", \"cruise\", \"ballistic\"}\n",
"\n",
"\n",
"def _rect(x0, x1, y0, y1):\n",
" return [(x0, y0), (x1, y0), (x1, y1), (x0, y1)]\n",
"\n",
"\n",
"def _bar(p, q, w):\n",
" (x0, y0), (x1, y1) = p, q\n",
" n = np.array([y0 - y1, x1 - x0]) / np.hypot(x1 - x0, y1 - y0) * w / 2\n",
" return [(x0 + n[0], y0 + n[1]), (x1 + n[0], y1 + n[1]), (x1 - n[0], y1 - n[1]), (x0 - n[0], y0 - n[1])]\n",
"\n",
"\n",
"# ๋ํ ์ ์(๋จ์ ์ขํ, ๊ธฐ์ ๋ฐฉํฅ = +y). ๋ชจ๋ ์ถ์ ๋ํ์ด๋ฉฐ ํน์ ๊ธฐ์ข
์ ๋ณธ๋จ์ง ์๋๋ค.\n",
"SHAPES = {\n",
" \"civil_quad\": {\"poly\": [_bar((-.55, -.55), (.55, .55), .1), _bar((-.55, .55), (.55, -.55), .1), _rect(-.15, .15, -.15, .15)],\n",
" \"circ\": [(-.55, -.55, .24), (.55, .55, .24), (-.55, .55, .24), (.55, -.55, .24)]},\n",
" \"civil_fw\": {\"poly\": [_rect(-.07, .07, -.8, .72), _rect(-.9, .9, .05, .25), _rect(-.33, .33, -.8, -.66)], \"circ\": []},\n",
" \"attack_fw\": {\"poly\": [[(0, .85), (.85, -.5), (.12, -.42), (0, -.6), (-.12, -.42), (-.85, -.5)]], \"circ\": []},\n",
" \"cruise\": {\"poly\": [_rect(-.07, .07, -.9, .7), [(-.07, .7), (0, .92), (.07, .7)], _rect(-.42, .42, -.05, .07),\n",
" [(-.07, -.9), (-.24, -.95), (-.07, -.74)], [(.07, -.9), (.24, -.95), (.07, -.74)]], \"circ\": []},\n",
" \"ballistic\": {\"poly\": [_rect(-.13, .13, -.85, .6), [(-.13, .6), (0, .95), (.13, .6)],\n",
" [(-.13, -.85), (-.34, -.95), (-.13, -.58)], [(.13, -.85), (.34, -.95), (.13, -.58)]], \"circ\": []},\n",
" \"bird\": {\"poly\": [[(-.9, .12), (-.5, .28), (-.12, .06), (0, .22), (.12, .06), (.5, .28), (.9, .12), (.5, .12),\n",
" (.12, -.1), (0, -.38), (-.12, -.1), (-.5, .12)]], \"circ\": []},\n",
"}\n",
"_S = 2\n",
"_g = (np.arange(IMG * _S) + 0.5) / _S\n",
"_GX, _GY = np.meshgrid(_g, _g)\n",
"_PTS = np.c_[_GX.ravel(), _GY.ravel()]\n",
"_PATHS = {c: [MplPath(np.array(p)) for p in s[\"poly\"]] for c, s in SHAPES.items()}\n",
"\n",
"\n",
"def draw(cls, rng, angle=None, scale=None, fore=None, blur=0.3, noise=4.0, contrast=1.0,\n",
" clutter=0.0, occlude=0.0, invert=False, shift=1.5):\n",
" \"\"\"ํด๋์ค 1๊ฐ ์ค๋ฃจ์ฃ ์ด๋ฏธ์ง(uint8 32x32)๋ฅผ ๊ทธ๋ฆฐ๋ค. ๋ฐฐ๊ฒฝ์ ๋ฐ์ ํ๋, ๋นํ์ฒด๋ ์ด๋์ด ๊ทธ๋ฆผ์.\"\"\"\n",
" angle = rng.uniform(0, 360) if angle is None else angle\n",
" scale = rng.uniform(.7, .95) if scale is None else scale\n",
" fore = rng.uniform(.85, 1.0) if fore is None else fore\n",
" th = np.radians(angle)\n",
" c0 = IMG / 2 + rng.uniform(-shift, shift, 2)\n",
" d = (_PTS - c0) / (scale * IMG / 2) # ์ด๋ฏธ์ง ์ขํ -> ๋ํ ์ขํ(์ญ๋ณํ)\n",
" u = d[:, 0] * np.cos(th) + d[:, 1] * np.sin(th)\n",
" v = (-d[:, 0] * np.sin(th) + d[:, 1] * np.cos(th)) / fore # ์๊ทผ ์ถ์(๊ธฐ์ ๋ฐฉํฅ์ผ๋ก ์งง์ ๋ณด์)\n",
" uv = np.c_[u, -v]\n",
" m = np.zeros(len(uv), bool)\n",
" near = np.flatnonzero(np.abs(uv).max(axis=1) <= 1.0) # ๋ํ์ด ์์ ์ ์๋ ์์ญ๋ง ๊ฒ์ฌ(์๋)\n",
" for p in _PATHS[cls]:\n",
" m[near] |= p.contains_points(uv[near])\n",
" for cx, cy, r in SHAPES[cls][\"circ\"]:\n",
" m |= (uv[:, 0] - cx) ** 2 + (uv[:, 1] - cy) ** 2 <= r * r\n",
" cov = m.reshape(IMG, _S, IMG, _S).mean(axis=(1, 3))\n",
" if occlude > 0: # ๋ถ๋ถ ๊ฐ๋ฆผ(๊ตฌ๋ฆ ์กฐ๊ฐ)\n",
" h = int(IMG * np.sqrt(occlude)); y0, x0 = rng.integers(8, IMG - 8 - h // 2, 2)\n",
" cov[y0:y0 + h, x0:x0 + h] = 0\n",
" bg = rng.uniform(180, 220) + np.linspace(-10, 10, IMG)[:, None]\n",
" fg = rng.uniform(30, 60)\n",
" img = bg * (1 - cov) + fg * cov\n",
" if clutter > 0: # ๊ตฌ๋ฆยท๋ฐฐ๊ฒฝ ์ผ๋ฃฉ\n",
" img -= clutter * 90 * np.clip(ndimage.gaussian_filter(rng.normal(0, 1, (IMG, IMG)), 3) * 4, 0, None)\n",
" if blur > 0:\n",
" img = ndimage.gaussian_filter(img, blur)\n",
" mean = img.mean()\n",
" img = mean + (img - mean) * contrast\n",
" img = img + rng.normal(0, noise, img.shape)\n",
" if invert: # ์ผ๊ฐ ์ด์(IR) ๊ทน์ฑ ๋ฐ์ \n",
" img = 255 - img\n",
" return np.clip(img, 0, 255).astype(np.uint8)\n",
"\n",
"\n",
"# ์ดฌ์ ์กฐ๊ฑด ํ๋ฆฌ์
: ๊ธฐ๋ณธ + ๋ณตํฉ ์ํ ์๋๋ฆฌ์ค\n",
"CONDITIONS = {\n",
" \"basic\": {},\n",
" \"offcenter\": {\"shift\": 5.0, \"fore\": (.6, .85)},\n",
" \"far\": {\"scale\": (.32, .45), \"blur\": .6},\n",
" \"blur\": {\"blur\": (1.2, 1.8)},\n",
" \"lowcon\": {\"contrast\": (.2, .35)},\n",
" \"clutter\": {\"clutter\": .8},\n",
" \"occlude\": {\"occlude\": .08},\n",
" \"oblique\": {\"fore\": (.35, .6)},\n",
" \"night\": {\"invert\": True, \"noise\": 10.0},\n",
" \"noise\": {\"noise\": 28.0},\n",
" \"combo\": {\"scale\": (.4, .55), \"blur\": (.8, 1.1), \"noise\": 12.0, \"contrast\": (.6, .8)},\n",
"}\n",
"\n",
"\n",
"def draw_cond(cls, cond, rng):\n",
" kw = {}\n",
" for k, v in CONDITIONS[cond].items():\n",
" kw[k] = rng.uniform(*v) if isinstance(v, tuple) else v\n",
" return draw(cls, rng, **kw)\n",
"\n",
"\n",
"def make_dataset(n_per_class, seed, conds=(\"basic\",)):\n",
" \"\"\"(์ด๋ฏธ์ง ๋ฐฐ์ด N x 32 x 32, ์ ๋ต ๋ผ๋ฒจ, ์กฐ๊ฑด) ๋ฐํ. ์กฐ๊ฑด์ conds์์ ๋ฒ๊ฐ์ ๊ณ ๋ฅธ๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" X, y, c = [], [], []\n",
" for cls in CLASSES:\n",
" for i in range(n_per_class):\n",
" cond = conds[i % len(conds)]\n",
" X.append(draw_cond(cls, cond, rng)); y.append(cls); c.append(cond)\n",
" return np.array(X), np.array(y), np.array(c)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์ ์ฒ๋ฆฌ(๋ฐฐ์น ๋ฒกํฐํ)ยทํํ ํน์ง ๊ณตํต ํจ์ ----\n",
"OUT = 24 # ํฌ๊ธฐ ์ ๊ทํ ๊ฒฐ๊ณผ(24x24)\n",
"NOISE_T = 12.0 # ์ก์ ์ถ์ ๊ฐ์ด ์ด๋ณด๋ค ํฌ๋ฉด 3x3 ์ค๊ฐ๊ฐ ํํฐ ์ ์ฉ(์
์ฒํยท์ผ์ ์ก์)\n",
"FEATS = [\"elong\", \"fill\", \"solidity\", \"n_parts\", \"end_gap\", \"mid_bulge\", \"w_max\", \"compact\"]\n",
"\n",
"\n",
"def preprocess(images, noise_t=NOISE_T):\n",
" \"\"\"N์ฅ ์ผ๊ด ์ฒ๋ฆฌ: ๊ฒฐ์ธก ๋ณด์ -> ๊ทน์ฑ(์ผ๊ฐ ๋ฐ์ ) ๋ณด์ -> ์กฐ๊ฑด๋ณ ์ก์ ์ ๊ฑฐ -> ๋ช
์ ๋๋น ๊ฐํ -> ๋ถํ -> ์ค๊ณฝ์ -> ๋ฐฉํฅยทํฌ๊ธฐ ์ ๊ทํ.\"\"\"\n",
" f = np.array(images, dtype=float).reshape(-1, IMG, IMG)\n",
" n = len(f)\n",
" bad = ~np.isfinite(f)\n",
" missing = bad.sum(axis=(1, 2))\n",
" if bad.any(): # ๊ฒฐ์ธก ํฝ์
: ์์ ์ค์๊ฐ์ผ๋ก ์ฑ์ด ๋ค ์ฃผ๋ณ 3x3 ์ค๊ฐ๊ฐ์ผ๋ก ๋ณด์ \n",
" med = np.array([np.median(x[np.isfinite(x)]) if np.isfinite(x).any() else 128.0 for x in f])\n",
" f = np.where(bad, med[:, None, None], f)\n",
" f = np.where(bad, ndimage.median_filter(f, size=(1, 3, 3)), f)\n",
" border = np.concatenate([f[:, 0], f[:, -1], f[:, :, 0], f[:, :, -1]], axis=1)\n",
" inverted = np.median(border, axis=1) < 128 # ๋ฐฐ๊ฒฝ์ด ์ด๋์ฐ๋ฉด ์ผ๊ฐ ์ด์์ผ๋ก ๋ณด๊ณ ๋ฐ์ \n",
" f[inverted] = 255 - f[inverted]\n",
" med3 = ndimage.median_filter(f, size=(1, 3, 3))\n",
" noise = 1.4826 * np.median(np.abs(f - med3).reshape(n, -1), axis=1)\n",
" noisy = noise > noise_t\n",
" f[noisy] = med3[noisy]\n",
" lo, hi = np.percentile(f.reshape(n, -1), [1, 99], axis=1)\n",
" e = np.clip((f - lo[:, None, None]) / np.maximum(hi - lo, 1e-6)[:, None, None], 0, 1) # ๋ช
์ ๋๋น ๊ฐํ\n",
" mask = e < _otsu(e)[:, None, None]\n",
" mask &= ndimage.uniform_filter(mask.astype(float), size=(1, 3, 3)) * 9 >= 2.5 # ๊ณ ๋ฆฝ ์ ์ก์ ์ ๊ฑฐ\n",
" edge = np.hypot(ndimage.sobel(e, axis=1), ndimage.sobel(e, axis=2)) # ์ค๊ณฝ์ (Sobel)\n",
" ok = (mask.sum(axis=(1, 2)) >= 4) & (hi - lo > 8)\n",
" return {\"enh\": e, \"mask\": mask, \"edge\": edge / np.maximum(edge.max(axis=(1, 2)), 1e-6)[:, None, None],\n",
" \"norm\": _normalize(1 - e, mask, ok), \"inverted\": inverted, \"noise\": noise, \"denoised\": noisy,\n",
" \"missing\": missing, \"contrast\": hi - lo, \"ok\": ok}\n",
"\n",
"\n",
"def _otsu(e, bins=64):\n",
" n = len(e)\n",
" idx = np.minimum((e * bins).astype(int), bins - 1) + bins * np.arange(n)[:, None, None]\n",
" hist = np.bincount(idx.ravel(), minlength=bins * n).reshape(n, bins).astype(float)\n",
" centers = (np.arange(bins) + 0.5) / bins\n",
" w = np.cumsum(hist, axis=1); m = np.cumsum(hist * centers, axis=1)\n",
" tot, mt = w[:, -1:], m[:, -1:]\n",
" between = (mt * w / tot - m) ** 2 / np.maximum(w * (tot - w), 1)\n",
" return (np.argmax(between, axis=1) + 1) / bins\n",
"\n",
"\n",
"def _normalize(obj, mask, ok):\n",
" \"\"\"์ฃผ์ถ ๋ฐฉํฅ์ ์ธ๋ก๋ก ๋๋ฆฌ๊ณ ๋นํ์ฒด ์์ญ๋ง 24x24๋ก ๋ค์ ์ํ๋งํ๋ค(ํ์ ยทํฌ๊ธฐยท์์น ๋ถ๋ณ). ํ ๋ฒ์ ๋ณด๊ฐ์ผ๋ก ์ผ๊ด ์ฒ๋ฆฌ.\"\"\"\n",
" n = len(obj)\n",
" w = mask.astype(float)\n",
" m00 = np.maximum(w.sum(axis=(1, 2)), 1)\n",
" yy, xx = np.mgrid[0:IMG, 0:IMG].astype(float)\n",
" cy, cx = (w * yy).sum(axis=(1, 2)) / m00, (w * xx).sum(axis=(1, 2)) / m00\n",
" dy, dx = yy - cy[:, None, None], xx - cx[:, None, None]\n",
" mu20, mu02, mu11 = (w * dy * dy).sum(axis=(1, 2)), (w * dx * dx).sum(axis=(1, 2)), (w * dy * dx).sum(axis=(1, 2))\n",
" th = 0.5 * np.arctan2(2 * mu11, mu20 - mu02)\n",
" a = np.stack([np.cos(th), np.sin(th)], 1) # ์ฃผ์ถ(y, x)\n",
" b = np.stack([-np.sin(th), np.cos(th)], 1) # ๋ถ์ถ\n",
" pa = np.where(mask, dy * a[:, 0, None, None] + dx * a[:, 1, None, None], 0)\n",
" pb = np.where(mask, dy * b[:, 0, None, None] + dx * b[:, 1, None, None], 0)\n",
" half = np.maximum(np.abs(pa).max(axis=(1, 2)), np.abs(pb).max(axis=(1, 2))) + 1.0\n",
" g = np.linspace(-1, 1, OUT)\n",
" U, V = np.meshgrid(g, g, indexing=\"ij\")\n",
" Y = cy[:, None, None] + half[:, None, None] * (U * a[:, 0, None, None] + V * b[:, 0, None, None])\n",
" X = cx[:, None, None] + half[:, None, None] * (U * a[:, 1, None, None] + V * b[:, 1, None, None])\n",
" N = np.broadcast_to(np.arange(n)[:, None, None], Y.shape)\n",
" out = ndimage.map_coordinates(obj * mask, [N, Y, X], order=1, cval=0.0)\n",
" skew = (out * (U ** 3)).sum(axis=(1, 2))\n",
" out[skew < 0] = out[skew < 0, ::-1] # ๋ฌด๊ฒ๊ฐ ์ ๋ฆฐ ์ชฝ์ ์๋ก(180๋ ๋ชจํธ์ฑ ์ ๊ฑฐ)\n",
" out[~ok] = 0\n",
" return np.clip(out, 0, 1)\n",
"\n",
"\n",
"def shape_features(mask):\n",
" \"\"\"๋ถํ ๋ง์คํฌ 1์ฅ์์ ํ์ ์ ๋ฌด๊ดํ ํํ ํน์ง 8์ข
์ ๊ณ์ฐํ๋ค.\"\"\"\n",
" if mask.sum() < 4:\n",
" return dict.fromkeys(FEATS, 0.0)\n",
" yx = np.argwhere(mask).astype(float)\n",
" c = yx - yx.mean(axis=0)\n",
" val, vec = np.linalg.eigh(np.cov(c.T) + 1e-6 * np.eye(2))\n",
" t, s = c @ vec[:, 1], c @ vec[:, 0]\n",
" length, width = np.ptp(t) + 1, np.ptp(s) + 1\n",
" corners = np.vstack([yx + d for d in ([.5, .5], [.5, -.5], [-.5, .5], [-.5, -.5])])\n",
" try:\n",
" hull = ConvexHull(corners).volume\n",
" except Exception:\n",
" hull = float(mask.sum())\n",
" bins = np.digitize(t, np.linspace(t.min(), t.max() + 1e-6, 4)[1:-1])\n",
" w = np.array([np.ptp(s[bins == k]) + 1 if (bins == k).any() else 0 for k in range(3)]) / length\n",
" boundary = mask & ~ndimage.binary_erosion(mask)\n",
" return {\"elong\": float(np.sqrt(val[1] / max(val[0], 1e-6))), \"fill\": float(mask.sum() / (length * width)),\n",
" \"solidity\": float(mask.sum() / max(hull, 1)), \"n_parts\": float(ndimage.label(mask)[1]),\n",
" \"end_gap\": float(abs(w[0] - w[2])), \"mid_bulge\": float(w[1] - max(w[0], w[2])),\n",
" \"w_max\": float(w.max()), \"compact\": float(boundary.sum() / np.sqrt(mask.sum()))}\n",
"\n",
"\n",
"def feature_matrix(images):\n",
" \"\"\"์ ์ฒ๋ฆฌ ๊ฒฐ๊ณผ, ํํ ํน์ง ํ๋ ฌ(N x 8), ์ ๊ทํ ์ด๋ฏธ์ง ํ๋ ฌ(N x 576)์ ๋๋ ค์ค๋ค.\"\"\"\n",
" P = preprocess(images)\n",
" F = np.array([[d[k] for k in FEATS] for d in map(shape_features, P[\"mask\"])])\n",
" return P, F, P[\"norm\"].reshape(len(F), -1)\n",
"\n",
"CONFIG = {\"augment_conditions\": [\"combo\", \"oblique\", \"occlude\", \"clutter\", \"far\"], \"extra_classes\": [\"cruise\", \"ballistic\", \"civil_fw\", \"attack_fw\"],\n",
" \"min_confidence\": 0.5, \"hidden_layer\": 96} # ๋ฏธ์
9 ์ฌ๋ณด์ ๊ฐ\n",
"CALLED = set()\n",
"\n",
"\n",
"def _tracer(frame, event, arg):\n",
" if event == \"call\" and frame.f_globals is globals():\n",
" CALLED.add(frame.f_code.co_name)\n",
"\n",
"\n",
"def validate(img):\n",
" a = np.asarray(img, dtype=float) if img is not None else None\n",
" return a is not None and a.shape == (IMG, IMG) and np.isfinite(a).all() and a.min() >= 0 and a.max() <= 255\n",
"\n",
"\n",
"def train():\n",
" X1, y1, _ = make_dataset(80, 5, tuple(CONDITIONS))\n",
" X2, y2, _ = make_dataset(40, 6, tuple(CONFIG[\"augment_conditions\"]))\n",
" rng = np.random.default_rng(7)\n",
" ex = CONFIG[\"extra_classes\"] * 20\n",
" X3 = np.array([draw_cond(c, CONFIG[\"augment_conditions\"][i % 5], rng) for i, c in enumerate(ex)])\n",
" X, y = np.r_[X1, X2, X3], np.r_[y1, y2, ex]\n",
" P, F, N = feature_matrix(X)\n",
" sc = StandardScaler().fit(F)\n",
" m = MLPClassifier((CONFIG[\"hidden_layer\"],), max_iter=200, early_stopping=True, random_state=0)\n",
" return m.fit(np.c_[N, sc.transform(F) * 0.3], [CLASSES.index(v) for v in y]), sc\n",
"\n",
"\n",
"def classify(m, sc, images):\n",
" P, F, N = feature_matrix(images)\n",
" prob = m.predict_proba(np.c_[N, sc.transform(F) * 0.3])\n",
" pred = np.array(CLASSES)[prob.argmax(1)]\n",
" conf = prob.max(1) * np.where(P[\"ok\"], 1, 0)\n",
" return pred, conf, np.where(conf >= 0.8, \"High\", np.where(conf >= CONFIG[\"min_confidence\"], \"Medium\", \"Low\"))\n",
"\n",
"\n",
"class Queue:\n",
" def __init__(self):\n",
" self.items, self.log = [], []\n",
"\n",
" def submit(self, rec):\n",
" prev = self.log[-1][\"hash\"] if self.log else \"GENESIS\"\n",
" body = {\"event\": \"SUBMIT\", **rec}\n",
" self.log.append({**body, \"prev\": prev, \"hash\": hashlib.sha256((prev + json.dumps(body, sort_keys=True)).encode()).hexdigest()})\n",
" self.items.append({**rec, \"status\": \"PENDING\"})\n",
"\n",
"\n",
"sys.settrace(_tracer)\n",
"t_start = time.perf_counter()\n",
"# 1. ํ์ต\n",
"model, scaler = train()\n",
"# 2. ์ด์ฉ ๋ฐ์ดํฐ(๊ต์ก์ฅ ๋ฐฐํฌ ๋ฌธํญ 300์ฅ) ๊ฒ์ฆ -> ์์ 3์ฅ ์ฐจ๋จ\n",
"X, y, cond = make_dataset(50, 41, tuple(CONDITIONS))\n",
"items = list(X)\n",
"items[5], items[77], items[150] = None, np.zeros((20, 20)), np.full((IMG, IMG), np.nan)\n",
"ok_idx = [i for i, im in enumerate(items) if validate(im)]\n",
"rejected = len(items) - len(ok_idx)\n",
"Xo, yo = np.array([items[i] for i in ok_idx]), y[ok_idx]\n",
"t0 = time.perf_counter()\n",
"pred, conf, grade = classify(model, scaler, Xo)\n",
"ms = (time.perf_counter() - t0) * 1000 / len(Xo)\n",
"judged = grade != \"Low\"\n",
"acc_all = (pred == yo).mean()\n",
"acc_judged = (pred[judged] == yo[judged]).mean()\n",
"# 3. ํด์ฆ ์ด์(๊ฐ์ ์ฅ๋ณ 20๋ช
x 10๋ฌธํญ) -> ์ค๋ต์ ํด์ค ๊ฒํ ๋๊ธฐ์ด(์๋ ๊ณต๊ฐ ์์)\n",
"rng = np.random.default_rng(9)\n",
"q, answers = Queue(), []\n",
"CONFUSE = {\"civil_fw\": \"attack_fw\", \"attack_fw\": \"civil_fw\", \"cruise\": \"ballistic\", \"ballistic\": \"cruise\", \"bird\": \"civil_fw\", \"civil_quad\": \"civil_fw\"}\n",
"for s in range(20):\n",
" for k in rng.choice(len(yo), 10, replace=False):\n",
" ans = yo[k] if rng.random() < 0.75 else CONFUSE[yo[k]]\n",
" answers.append(ans == yo[k])\n",
" if ans != yo[k]:\n",
" q.submit({\"learner\": f\"L{s + 1:02d}\", \"answer\": str(yo[k]), \"chosen\": str(ans), \"ai_grade\": str(grade[k])})\n",
"sys.settrace(None)\n",
"total = time.perf_counter() - t_start\n",
"\n",
"defined = {n for n, f in globals().items() if isinstance(f, types.FunctionType) and f.__module__ == __name__ and not n.startswith(\"_tracer\")}\n",
"defined |= {\"submit\", \"__init__\"}\n",
"coverage = len(CALLED & defined) / len(defined)\n",
"chk = {\n",
" \"์์ ๋ฐ์ดํฐ 3๊ฑด ์ฐจ๋จ\": rejected == 3,\n",
" \"ํ๋ณ ๋์ 297์ฅ ์ฒ๋ฆฌ\": len(Xo) == 297,\n",
" \"์ ์ฒด ๋ถ๋ฅ ์ ํ๋ 85% ์ด์\": acc_all >= 0.85,\n",
" \"ํ๋ณ(Medium ์ด์) ์ ํ๋ 95% ์ด์\": acc_judged >= 0.95,\n",
" \"High ๋ฑ๊ธ ์ ํ๋ >= Medium ๋ฑ๊ธ ์ ํ๋\": (pred == yo)[grade == \"High\"].mean() >= (pred == yo)[grade == \"Medium\"].mean(),\n",
" \"์ฅ๋น ์ฒ๋ฆฌ 200 ms ์ด๋ด\": ms <= 200,\n",
" \"์ค๋ต ํด์ค ์๋ ๊ณต๊ฐ 0๊ฑด\": all(i[\"status\"] == \"PENDING\" for i in q.items),\n",
" \"๊ฐ์ฌ ๋ก๊ทธ ๊ฑด์ ์ผ์น\": len(q.log) == len(q.items) == sum(not a for a in answers),\n",
" \"ํจ์ ์ปค๋ฒ๋ฆฌ์ง 85% ์ด์\": coverage >= 0.85,\n",
" \"์ ์ฒด ์คํ 60์ด ์ด๋ด\": total < 60,\n",
"}\n",
"for k, v in chk.items():\n",
" print(f\"[{'PASS' if v else 'FAIL'}] {k}\")\n",
"passed = sum(chk.values())\n",
"print(f\"์์ฒด ์ ๊ฒ {passed}/{len(chk)} ํต๊ณผ, ํจ์ ์ปค๋ฒ๋ฆฌ์ง {coverage:.0%} ({len(CALLED & defined)}/{len(defined)}), ํตํฉ ์คํ {'์ฑ๊ณต' if passed == len(chk) else '์ ๊ฒ ํ์'}\")\n",
"\n",
"fig, ax = plt.subplots(2, 2, figsize=(11, 8))\n",
"for j in range(6):\n",
" sub = ax[0, 0].inset_axes([j % 3 / 3, 1 - (j // 3 + 1) / 2, 0.32, 0.48])\n",
" i = np.flatnonzero(yo == CLASSES[j])[0]\n",
" sub.imshow(Xo[i], cmap=\"gray\"); sub.set_title(f\"{CLASSES[j]}\\n{pred[i]} ({grade[i]})\", fontsize=7); sub.axis(\"off\")\n",
"ax[0, 0].axis(\"off\"); ax[0, 0].set_title(\"Sample items: true / predicted (grade)\")\n",
"g = pd.Series(grade).value_counts().reindex([\"High\", \"Medium\", \"Low\"]).fillna(0)\n",
"ax[0, 1].bar(g.index, g.values, color=[\"tab:green\", \"tab:orange\", \"tab:red\"]); ax[0, 1].set_title(\"Confidence grade of deployed items\")\n",
"acc_c = pd.Series(pred == yo).groupby(cond[ok_idx]).mean()\n",
"ax[1, 0].bar(acc_c.index, acc_c.values); ax[1, 0].tick_params(axis=\"x\", rotation=40); ax[1, 0].set_title(\"AI accuracy by condition\")\n",
"ax[1, 1].axis(\"off\")\n",
"ax[1, 1].text(0, .9, f\"Items {len(Xo)} (+{rejected} rejected)\\nAI accuracy {acc_all:.1%}\\nLearner acc {np.mean(answers):.1%}\\n\"\n",
" f\"Review queue {len(q.items)} pending\\nCoverage {coverage:.0%}\\nSelf-check {passed}/{len(chk)}\", family=\"monospace\", fontsize=12, va=\"top\")\n",
"plt.suptitle(\"TRN03 silhouette ID app - integrated run (training data only)\"); plt.tight_layout(); plt.show()\n",
"\n",
"top = pd.Series([f\"{i['answer']}->{i['chosen']}\" for i in q.items]).value_counts().head(2)\n",
"print(f\"\"\"\n",
"[๋ถ๋ ๊ต์ก ๋ฐฐํฌ ๋ธ๋ฆฌํ ์์ฝ - ๊ต์ก์ฉ๊ฐ์์๋ฃ]\n",
"1. ๊ตฌ์ฑ: ํฉ์ฑ ์ถ์ ์ค๋ฃจ์ฃ 6์ข
(์ํ 3์ข
), ๋ฌธํญ {len(items)}์ฅ ์ค {len(Xo)}์ฅ ๋ฐฐํฌ, ์์ {rejected}์ฅ ์๋ ์ฐจ๋จ\n",
"2. AI ์ฑ๋ฅ: ์ ์ฒด ์ ํ๋ {acc_all:.1%}, ์ ๋ขฐ๋ Medium ์ด์ ํ๋ณ ์ ํ๋ {acc_judged:.1%}, Low {int((~judged).sum())}์ฅ์ ๊ต๊ด ํ์ธ, ์ฅ๋น {ms:.2f} ms\n",
"3. ํ์ต ์ด์: ๊ฐ์ ์ฅ๋ณ 20๋ช
์ ๋ต๋ฅ {np.mean(answers):.1%}, ๋ค๋น๋ ์ค๋ต {', '.join(f'{k}({v})' for k, v in top.items())}\n",
"4. ๊ต๊ด ๊ฒํ : ์ค๋ต ํด์ค {len(q.items)}๊ฑด ๊ฒํ ๋๊ธฐ, ์๋ ๊ณต๊ฐ 0๊ฑด, ๊ฐ์ฌ ๋ก๊ทธ {len(q.log)}๊ฑด\n",
"5. ์ด์ฉ ์ ์: ์ค๋ฃจ์ฃ์ ๊ธฐ๋ณธ ๋ํ์ผ๋ก ๋ง๋ ํ์ต์ฉ ์ถ์ ์ด๋ฏธ์ง์ด๋ฉฐ ์ค์ ๊ธฐ์ข
์๋ณ ๊ต๋ฒ์ ๋์ฒดํ์ง ์๋๋ค. ์ค์ ํ์ ํ๋จ์ ์ ์ ๊ต์ก๊ณผ ์งํ๊ณํต์ ๋ฐ๋ฅธ๋ค.\n",
"6. ํ์ฅ ๋ก๋๋งต: ํ์ต ๊ธฐ๋ก ๋ด๋ณด๋ด๊ธฐ API(๋ฌธํญ IDยท์๋ตยท์์์๊ฐ), ์ธ์ ๊ต์ก ์ฒด๊ณ์ ๋ฌธํญ ์ํ ๊ณต์ ์ธํฐํ์ด์ค\"\"\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
10 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[PASS] ์์ ๋ฐ์ดํฐ 3๊ฑด ์ฐจ๋จ\n",
"[PASS] ํ๋ณ ๋์ 297์ฅ ์ฒ๋ฆฌ\n",
"[PASS] ์ ์ฒด ๋ถ๋ฅ ์ ํ๋ 85% ์ด์\n",
"[PASS] ํ๋ณ(Medium ์ด์) ์ ํ๋ 95% ์ด์\n",
"[PASS] High ๋ฑ๊ธ ์ ํ๋ >= Medium ๋ฑ๊ธ ์ ํ๋\n",
"[PASS] ์ฅ๋น ์ฒ๋ฆฌ 200 ms ์ด๋ด\n",
"[PASS] ์ค๋ต ํด์ค ์๋ ๊ณต๊ฐ 0๊ฑด\n",
"[PASS] ๊ฐ์ฌ ๋ก๊ทธ ๊ฑด์ ์ผ์น\n",
"[PASS] ํจ์ ์ปค๋ฒ๋ฆฌ์ง 85% ์ด์\n",
"[PASS] ์ ์ฒด ์คํ 60์ด ์ด๋ด\n",
"์์ฒด ์ ๊ฒ 10/10 ํต๊ณผ, ํจ์ ์ปค๋ฒ๋ฆฌ์ง 87% (13/15), ํตํฉ ์คํ ์ฑ๊ณต\n",
"\n",
"[๋ถ๋ ๊ต์ก ๋ฐฐํฌ ๋ธ๋ฆฌํ ์์ฝ - ๊ต์ก์ฉ๊ฐ์์๋ฃ]\n",
"1. ๊ตฌ์ฑ: ํฉ์ฑ ์ถ์ ์ค๋ฃจ์ฃ 6์ข
(์ํ 3์ข
), ๋ฌธํญ 300์ฅ ์ค 297์ฅ ๋ฐฐํฌ, ์์ 3์ฅ ์๋ ์ฐจ๋จ\n",
"2. AI ์ฑ๋ฅ: ์ ์ฒด ์ ํ๋ 85.2%, ์ ๋ขฐ๋ Medium ์ด์ ํ๋ณ ์ ํ๋ 96.7%, Low 55์ฅ์ ๊ต๊ด ํ์ธ, ์ฅ๋น 1.47 ms\n",
"3. ํ์ต ์ด์: ๊ฐ์ ์ฅ๋ณ 20๋ช
์ ๋ต๋ฅ 75.0%, ๋ค๋น๋ ์ค๋ต ballistic->cruise(12), civil_quad->civil_fw(11)\n",
"4. ๊ต๊ด ๊ฒํ : ์ค๋ต ํด์ค 50๊ฑด ๊ฒํ ๋๊ธฐ, ์๋ ๊ณต๊ฐ 0๊ฑด, ๊ฐ์ฌ ๋ก๊ทธ 50๊ฑด\n",
"5. ์ด์ฉ ์ ์: ์ค๋ฃจ์ฃ์ ๊ธฐ๋ณธ ๋ํ์ผ๋ก ๋ง๋ ํ์ต์ฉ ์ถ์ ์ด๋ฏธ์ง์ด๋ฉฐ ์ค์ ๊ธฐ์ข
์๋ณ ๊ต๋ฒ์ ๋์ฒดํ์ง ์๋๋ค. ์ค์ ํ์ ํ๋จ์ ์ ์ ๊ต์ก๊ณผ ์งํ๊ณํต์ ๋ฐ๋ฅธ๋ค.\n",
"6. ํ์ฅ ๋ก๋๋งต: ํ์ต ๊ธฐ๋ก ๋ด๋ณด๋ด๊ธฐ API(๋ฌธํญ IDยท์๋ตยท์์์๊ฐ), ์ธ์ ๊ต์ก ์ฒด๊ณ์ ๋ฌธํญ ์ํ ๊ณต์ ์ธํฐํ์ด์ค\n",
"```\n",
" \n"
]
}
],
"metadata": {
"colab": {
"provenance": []
},
"kernelspec": {
"display_name": "Python 3",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 0
}