{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# ๐ฏ [QLT03] ๊ตฐ์ํ ์ ์์ ๋ณด ์ด์์น ์๋๊ฒ์ฆ Tool\n",
"### Google Colab ์ค์ต ํ๊ฒฝ (ํ์งยท๊ท๊ฒฉยท๊ฒ์ฌ / Level 3)\n",
"\n",
"[](https://colab.research.google.com/github/edunlab/milicontent2/blob/main/uploads/QLT03/QLT03_solution.ipynb)\n",
"\n",
"---\n",
"\n",
"## ๐ 1. ํ๋ก์ ํธ ๊ฐ์ ๋ฐ ๋ฐฐ๊ฒฝ\n",
"\n",
"| ํญ๋ชฉ | ๋ด์ฉ |\n",
"| :--- | :--- |\n",
"| **๊ณผ์ ๋ถ๋ฅ** | ํ์งยท๊ท๊ฒฉยท๊ฒ์ฌ (Level 3) |\n",
"| **ํ์ต ์์ ์๊ฐ** | 10๊ฐ ๋ฏธ์
(5์ผ / ์ด 28.5์๊ฐ) |\n",
"| **ํ๊ฒฝ ์ฌ์** | Python 3.10+, numpy, pandas, matplotlib, scipy, scikit-learn |\n",
"\n",
"### ๐ ํ๋ก์ ํธ ๋ชฉํ\n",
"๊ตฐ์์ฅ๋น์ ์ธ๊ด ๊ฒ์ฌ๋ ๊ฒ์ฌ๊ด์ ๋๊ณผ ๊ฒฝํ์ ํฌ๊ฒ ์์กดํด ํ์ ํธ์ฐจ๊ฐ ์๊ธฐ๊ณ ๊ฒ์ฌ ๋ณ๋ชฉ์ด ๋๊ธฐ ์ฝ๋ค. ๋ณธ ํ๋ก์ ํธ๋ ๊ท๊ฒฉยท์ ์ ์ ํฉ์ฑ๊ณผ ๊ฒ์ฌ ๋ณ๋ชฉ, ์ด์ ์ ํธ, ์ค๋ช
๊ทผ๊ฑฐ, ํ์ง ๊ฐ์ ํจ๊ณผ๋ฅผ ํจ๊ป ์ตํ๋๋ก, ๊ต์ก์ฉ ๊ฐ์ ์ธ๊ด ์ฌ์ง(๊ท ์ดยท๋ถ์ยทํ์ยท๋์ )์ ์ฝ๋๋ก ๋ง๋ค์ด ๊ฒฐํจ ํ์ ๊ณผ ์์๋ยท์ ๋น ๋ฑ๊ธ ์ฐ์ ์ ์๋ํํ๋ค. ํ์ต์๋ ์ฌ์ง ๋ฐ์ดํฐ์
๊ณผ ์ ์ ๊ธฐ๋ก ์ ์, ์กฐ๋ช
๋ณด์ ยทํด์๋ ํ์คํยท๋ฐ์ค ์ ์ , 1์ฐจ ํ๋ณ Baseline, ๋ถํ ๊ธฐ๋ฐ ๋ฉด์ ยท๊น์ดยท์์๋ ๋ชจ๋ธ, ์ ๋น ๋ฑ๊ธ ๋ถ๋ฅ์ ์ ๋ขฐ๋ ์ง์, ์ฌ์ง ์
๋ก๋ ํ์ ํ๋ฉด๊ณผ ๊ทผ๊ฑฐ ์ค๋ช
, ๊ฒ์ฌ๊ด ์๋ ๋ณด์ ๊ณผ ๊ฒฐ์ฌ(HITL), ์งํ ํผ์
์คํธ๋ ์ค ์ํ, AAR ์ฌ๋ณด์ ๊ณผ ROI, ์ฌํ์ต ํ์ดํ๋ผ์ธ ํตํฉ์ ์ฐจ๋ก๋ก ์ํํ๋ค. ์ต์ข
๋ชฉํ๋ ์ฌ์ง์ผ๋ก ๊ฒฐํจ๊ณผ ์์๋๋ฅผ ํ์ ํ๊ณ ์ฌ์ง ํ์ ๊ณผ ๊ด๋ฆฌ ๊ธฐ๋ก์ ๋ถ์ผ์น๋ฅผ ์ด์ ์ ํธ๋ก ํ๋ฅํ๋, ์ต์ข
์ ๋น ๋ฑ๊ธ์ ๊ฒ์ฌ๊ด์ด ํ์ ํ๋ 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",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ฌ์ง, ์ฅ๋น ์ ์(๊ด๋ฆฌ ๊ธฐ๋ก), ๊ฒฐํจ ๋ผ๋ฒจ, ํ์ ๊ฒฐ๊ณผ์ ์คํค๋ง์ ํ์ฉ๊ฐ์ ์ ํ๊ณ ๊ฒฐํจ ์ ํ๋ณ ๋ผ๋ฒจ๋ง ๊ธฐ์ค์ ๋ฌธ์ฅ์ผ๋ก ์ ๋ฆฌํ๊ณ ๊ท๊ฒฉํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `docs/qlt03_logistics_domain_spec.md ๋ฐ ๋ฐ์ดํฐ ๋์
๋๋ฆฌ` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๊ตฐ์ ์ค๋ฌด ์
๋ฌด ํ๋ก์ธ์ค์์ ์ผ์น์ฑ ๋ฐ ๋ถ์ ๋ชฉํ์ ๋ช
ํ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ์
๋ฌด ํ๋ก์ธ์ค ๋งต ์์ฑ ์๋ฃ, ํ์ ํผ์ฒ(Feature) ๋ชฉ๋ก 20๊ฐ ์ด์ ์ ์\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ๋๋ฉ์ธ ๋ถ์์ ๋ฐ ๋ฐ์ดํฐ ์๊ตฌ๋ช
์ธ์ ์น์ธ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ๊ตฐ์ ๋ด๋น ์ค๋ฌด์ ์ธํฐ๋ทฐ ๋ฐ ๊ธฐ์กด ์ ์ฐ์ฒด๊ณ ๋ฐ์ดํฐ ๊ตฌ์กฐ ๋์กฐ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์ฅ๋น ์ธ๊ด ๊ฒฐํจ(๊ท ์ดยท๋ถ์ยทํ์ยท๋์ ) ์ฌ์ง ๋ฐ์ดํฐ์
์ ๋ผ๋ฒจ๋ง ๊ธฐ์ค, ์ฌ์งยท์ฅ๋น ์ ์(๊ด๋ฆฌ ๊ธฐ๋ก)ยท๊ฒฐํจ ๋ผ๋ฒจยทํ์ ๊ฒฐ๊ณผ์ ์คํค๋ง์ ๋ฐ์ดํฐ ๋์
๋๋ฆฌ(ํผ์ฒ 20๊ฐ ์ด์), ์
๋ฌด ํ๋ก์ธ์ค ๋งต์ ์ ์ํ๊ณ ๊ฐ์ ์ํ๋ก ์ ์ ์ ํฉ์ฑ์ ๊ฒ์ฆํ๋ค.\"\"\"\n",
"import json\n",
"import re\n",
"import numpy as np\n",
"import pandas as pd\n",
"\n",
"# 1) ์
๋ฌด ํ๋ก์ธ์ค ๋งต: ์ดฌ์์์ ์ ๋น ๋ฑ๊ธ ์น์ธยท์ ์ ํ๋ฅ๊น์ง\n",
"PROCESS = [(\"์ดฌ์\", \"์ฌ์ง ID, ์ดฌ์์ผ, ์ดฌ์ ์กฐ๊ฑด(์กฐ๋ช
ยท์ค์ผ)\"), (\"์ ์ ๋์กฐ\", \"์ฅ๋น ์๋ณ์ฝ๋, ์ ์กฐ์ฐ๋, ๊ฐ๋์๊ฐ, ๊ธฐ๋ก์ ์ ๋น ๋ฑ๊ธ\"),\n",
" (\"์ ์ฒ๋ฆฌ\", \"์กฐ๋ช
๋ณด์ , ํด์๋ ํ์คํ, ๊ฒฐํจ ์์ญ(๋ฐ์ด๋ฉ ๋ฐ์ค)\"), (\"๊ฒฐํจ ํ๋ณ\", \"๊ฒฐํจ ์ ํ, ๊ฒฐํจ ๋ฉด์ , ์ถ์ ๊น์ด\"),\n",
" (\"์์๋ ์ฐ์ \", \"์์๋ ์ ์(0~100), ํ์ ์ ๋ขฐ๋\"), (\"๊ฒ์ฌ๊ด ๋ณด์ \", \"์๋ ๋ณด์ ์ฌ๋ถ, ๋ณด์ ์ฌ์ \"),\n",
" (\"์ ๋น ๋ฑ๊ธ ์น์ธ\", \"ํ์ฅยท์ผ์ ยท์ฐฝ์ ๋น ๋ฑ๊ธ, ์น์ธ์, ์น์ธ ์๊ฐ\"), (\"์ ์ ํ๋ฅ\", \"ํ์ ๊ฒฐ๊ณผ์ ๊ธฐ๋ก์ ๋ฑ๊ธ์ ๋ถ์ผ์น, ์ ์ ์์ ์์ฒญ\"), (\"์ฌํ๊ฒํ (AAR)\", \"์คํ ์์ธ, ํ๋ผ๋ฏธํฐ ์ฌ๋ณด์ ์ด๋ ฅ\")]\n",
"\n",
"# 2) ๋ผ๋ฒจ๋ง ๊ธฐ์ค(๋ฐ์ดํฐ ๋์
๋๋ฆฌ์ ์ผ๋ถ)\n",
"LABEL_RULES = {\"crack\": \"์ ํ ์ด๋์ด ๊ท ์ด, ๊ธธ์ด 10px ์ด์\", \"corrosion\": \"๋ถ์ ๊ฐ์ ๋ฉด, ๋ฉด์ 12px ์ด์\", \"damage\": \"ํจ๋ชฐยทํ๋จ ์ค๊ณฝ(๋ฐ์ ํ
๋๋ฆฌ ํฌํจ)\",\n",
" \"leak\": \"์ด๋ก๊ณ ๋ฒ์ง๋ ์ก์ฒด ์๊ตญ(์๋ ๋ฐฉํฅ ํ๋ฆ)\", \"none\": \"๊ฒฐํจ ์์(๋ฌดํดํ ์ผ๋ฃฉยท๊ธํ์ ๊ฒฐํจ์ผ๋ก ๋ผ๋ฒจํ์ง ์์)\"}\n",
"\n",
"# 3) ์คํค๋ง: ๊ทธ๋ฃน / ํ๋ / ์๋ฃํ / ๊ท์น / ์ค๋ช
\n",
"S = {\n",
" \"photo\": {\n",
" \"img_id\": (str, {\"pattern\": r\"^IMG\\d{6}$\"}, \"์ฌ์ง ID\"),\n",
" \"shot_day\": (int, {\"min\": 0, \"max\": 400}, \"์ดฌ์์ผ(์ ๊ฒ ์์ ํ ๊ฒฝ๊ณผ์ผ)\"),\n",
" \"width\": (int, {\"enum\": [64]}, \"ํ์ค ํด์๋ ๊ฐ๋ก(px)\"),\n",
" \"height\": (int, {\"enum\": [64]}, \"ํ์ค ํด์๋ ์ธ๋ก(px)\"),\n",
" \"channels\": (int, {\"enum\": [3]}, \"์ฑ๋ ์(RGB)\"),\n",
" \"brightness\": (float, {\"min\": 0.05, \"max\": 1.0}, \"ํ๊ท ๋ฐ๊ธฐ(0~1)\"),\n",
" \"mud_ratio\": (float, {\"min\": 0, \"max\": 1}, \"์งํยท์ค์ผ ๊ฐ๋ฆผ ๋น์จ(์ถ์ )\"),\n",
" },\n",
" \"spec\": {\n",
" \"equip_id\": (str, {\"pattern\": r\"^VX-\\d{4}-\\d{4}$\"}, \"์ฅ๋น ์๋ณ์ฝ๋(๊ตญ๋ฐฉ ํ์ค ์ฝ๋ ํ์์ ๋ชจ์ฌํ ๊ฐ์ ์ฝ๋)\"),\n",
" \"equip_type\": (str, {\"enum\": [\"vehicle\", \"generator\", \"comm\", \"trailer\"]}, \"์ฅ๋น ๊ตฌ๋ถ\"),\n",
" \"mfg_year\": (int, {\"min\": 1990, \"max\": 2026}, \"์ ์กฐ์ฐ๋(๊ฐ์)\"),\n",
" \"op_hours\": (float, {\"min\": 0, \"max\": 30000}, \"๋์ ๊ฐ๋์๊ฐ(h)\"),\n",
" \"env\": (str, {\"enum\": [\"coastal\", \"inland\"]}, \"์ด์ฉ ํ๊ฒฝ\"),\n",
" \"reg_grade\": (int, {\"enum\": [1, 2, 3]}, \"๊ด๋ฆฌ ๊ธฐ๋ก์ ์ง์ ์ ๋น ๋ฑ๊ธ(1 ํ์ฅ, 2 ์ผ์ , 3 ์ฐฝ์ ๋น)\"),\n",
" },\n",
" \"label\": {\n",
" \"defect_class\": (str, {\"enum\": [\"none\", \"crack\", \"corrosion\", \"damage\", \"leak\"]}, \"๊ฒฐํจ ์ ํ\"),\n",
" \"bbox_x\": (int, {\"min\": 0, \"max\": 63}, \"๊ฒฐํจ ๋ฐ์ค ์ข์๋จ x\"),\n",
" \"bbox_y\": (int, {\"min\": 0, \"max\": 63}, \"๊ฒฐํจ ๋ฐ์ค ์ข์๋จ y\"),\n",
" \"bbox_w\": (int, {\"min\": 1, \"max\": 64}, \"๊ฒฐํจ ๋ฐ์ค ํญ\"),\n",
" \"bbox_h\": (int, {\"min\": 1, \"max\": 64}, \"๊ฒฐํจ ๋ฐ์ค ๋์ด\"),\n",
" \"emergency\": (bool, {}, \"๊ธด๊ธ ์ ๊ฒ ์์ฒญ ์ฌ๋ถ(ํ์ง ๋ถ๋์ด์ด๋ ๋ณด์กด)\"),\n",
" },\n",
" \"result\": {\n",
" \"area_frac\": (float, {\"min\": 0, \"max\": 1}, \"๊ฒฐํจ ๋ฉด์ ๋น์จ - ์์ธก๋์ 1\"),\n",
" \"depth_mm\": (float, {\"min\": 0, \"max\": 30}, \"์ถ์ ๊ฒฐํจ ๊น์ด - ์์ธก๋์ 2\"),\n",
" \"damage_idx\": (float, {\"min\": 0, \"max\": 100}, \"์์๋ ์ ์ - ์์ธก๋์ 3\"),\n",
" \"grade\": (int, {\"enum\": [1, 2, 3]}, \"์ ๋น ๋ฑ๊ธ(ํ์ฅ/์ผ์ /์ฐฝ์ ๋น) - ์์ธก๋์ 4\"),\n",
" \"confidence\": (float, {\"min\": 0, \"max\": 1}, \"ํ์ ์ ๋ขฐ๋ ์ง์\"),\n",
" },\n",
"}\n",
"\n",
"\n",
"def check(name, v, typ, rule):\n",
" # [TODO : ํ์ ์ค์ต 1] ์ฅ๋น ์ธ๊ด ๊ฒฐํจ(๊ท ์ด, ๋ถ์, ํ์, ๋์ ) ๊ณ ํด์๋ ์ฌ์ง ๋ฐ์ดํฐ์
๋ฐ ๋ผ๋ฒจ๋ง ์ ์ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def validate(rec):\n",
" # [TODO : ํ์ ์ค์ต 1] ์ฅ๋น ์ธ๊ด ๊ฒฐํจ(๊ท ์ด, ๋ถ์, ํ์, ๋์ ) ๊ณ ํด์๋ ์ฌ์ง ๋ฐ์ดํฐ์
๋ฐ ๋ผ๋ฒจ๋ง ์ ์ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"rng = np.random.default_rng(1)\n",
"recs = []\n",
"for i in range(20):\n",
" g = int(rng.integers(1, 4))\n",
" recs.append({\n",
" \"photo\": {\"img_id\": f\"IMG{i:06d}\", \"shot_day\": int(rng.integers(0, 90)), \"width\": 64, \"height\": 64, \"channels\": 3,\n",
" \"brightness\": float(round(rng.uniform(0.2, 0.9), 2)), \"mud_ratio\": float(round(rng.uniform(0, 0.3), 2))},\n",
" \"spec\": {\"equip_id\": f\"VX-{rng.integers(1000, 9999)}-{rng.integers(1000, 9999)}\", \"equip_type\": str(rng.choice([\"vehicle\", \"generator\", \"comm\", \"trailer\"])),\n",
" \"mfg_year\": int(rng.integers(2000, 2025)), \"op_hours\": float(round(rng.uniform(100, 9000), 0)), \"env\": str(rng.choice([\"coastal\", \"inland\"])),\n",
" \"reg_grade\": int(np.clip(g + rng.integers(-1, 2), 1, 3))},\n",
" \"label\": {\"defect_class\": str(rng.choice([\"none\", \"crack\", \"corrosion\", \"damage\", \"leak\"])), \"bbox_x\": int(rng.integers(0, 30)), \"bbox_y\": int(rng.integers(0, 30)),\n",
" \"bbox_w\": int(rng.integers(5, 30)), \"bbox_h\": int(rng.integers(5, 30)), \"emergency\": bool(rng.random() < 0.05)},\n",
" \"result\": {\"area_frac\": float(round(rng.uniform(0, 0.2), 3)), \"depth_mm\": float(round(rng.uniform(0, 6), 1)), \"damage_idx\": float(round(rng.uniform(0, 100), 1)),\n",
" \"grade\": g, \"confidence\": float(round(rng.uniform(0.5, 1), 2))}})\n",
"ok = sum(not validate(r) for r in recs)\n",
"print(f\"์ ์ ์ํ {len(recs)}๊ฑด ์ค ๊ฒ์ฆ ํต๊ณผ {ok}๊ฑด, ์ค๋ฅ {len(recs) - ok}๊ฑด\")\n",
"\n",
"# 5) ์ค๋ฅ ์ฃผ์
์ํ\n",
"bad = json.loads(json.dumps(recs[0]))\n",
"bad[\"spec\"][\"mfg_year\"] = 2085 # ์ ์ ๊ฐ ๋ฒ์ ์ด์(๋ฏธ๋ ์ ์กฐ์ฐ๋)\n",
"bad[\"label\"][\"bbox_x\"] = 60; bad[\"label\"][\"bbox_w\"] = 20 # ๋ฐ์ค๊ฐ ์ฌ์ง ๋ฐ์ผ๋ก ๋๊ฐ\n",
"bad[\"result\"][\"grade\"] = 5 # ํ์ฉ๋์ง ์์ ๋ฑ๊ธ\n",
"del bad[\"spec\"][\"op_hours\"] # ๊ฐ๋์๊ฐ ๋๋ฝ\n",
"bad[\"photo\"][\"brightness\"] = \"์ด๋์\" # ์๋ฃํ ์ค๋ฅ\n",
"print(\"\\n[์ค๋ฅ ์ฃผ์
๊ฒฐ๊ณผ]\")\n",
"for e in validate(bad):\n",
" print(\" -\", e)\n",
"mis = json.loads(json.dumps(recs[1])); mis[\"result\"][\"grade\"] = 3; mis[\"spec\"][\"reg_grade\"] = 1\n",
"print(\"์ ์ ๋ถ์ผ์น ์ํ:\", validate(mis))\n",
"\n",
"# 6) ํ๋ก์ธ์ค ๋งต๊ณผ ๋ฐ์ดํฐ ๋์
๋๋ฆฌ\n",
"print(\"\\n[์
๋ฌด ํ๋ก์ธ์ค ๋งต]\")\n",
"print(\" -> \".join(f\"{i}.{p}\" for i, (p, _) in enumerate(PROCESS, 1)))\n",
"for p, d in PROCESS:\n",
" print(f\" {p:10s}: {d}\")\n",
"print(\"\\n[๋ผ๋ฒจ๋ง ๊ธฐ์ค]\")\n",
"for k, v in LABEL_RULES.items():\n",
" print(f\" {k:10s}: {v}\")\n",
"rows = [(g, k, t.__name__, str(r) if r else \"-\", d) for g, f in S.items() for k, (t, r, d) in f.items()]\n",
"dd = pd.DataFrame(rows, columns=[\"๊ทธ๋ฃน\", \"ํ๋\", \"์๋ฃํ\", \"๊ท์น\", \"์ค๋ช
\"])\n",
"print(f\"\\n[๋ฐ์ดํฐ ๋์
๋๋ฆฌ] ํผ์ฒ {len(dd)}๊ฐ (๋ชฉํ 20๊ฐ ์ด์: {'๋ฌ์ฑ' if len(dd) >= 20 else '๋ฏธ๋ฌ'})\")\n",
"print(dd[[\"๊ทธ๋ฃน\", \"ํ๋\", \"์๋ฃํ\", \"์ค๋ช
\"]].to_string(index=False))\n",
"print(\"\\n์์ธก ๋์: ๊ฒฐํจ ๋ฉด์ , ์ถ์ ๊น์ด, ์์๋ ์ ์, ์ ๋น ๋ฑ๊ธ. ์ ์ ํ์ฉ: ์ฌ์ง ํ์ ๊ณผ ๊ด๋ฆฌ ๊ธฐ๋ก ๋ฑ๊ธ์ด ํฌ๊ฒ ๋ค๋ฅด๋ฉด ์ ์ ์ด์ ์ ํธ๋ก ํ์\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
01 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"์ ์ ์ํ 20๊ฑด ์ค ๊ฒ์ฆ ํต๊ณผ 20๊ฑด, ์ค๋ฅ 0๊ฑด\n",
"\n",
"[์ค๋ฅ ์ฃผ์
๊ฒฐ๊ณผ]\n",
" - photo.brightness: ์๋ฃํ ์ค๋ฅ(str)\n",
" - spec.mfg_year: ์ต๋๊ฐ 2026 ์ด๊ณผ\n",
" - spec.op_hours: ํ์ ํ๋ ๋๋ฝ\n",
" - result.grade: ํ์ฉ๋์ง ์์ ๊ฐ '5'\n",
" - label.bbox: ๋ฐ์ค๊ฐ ์ด๋ฏธ์ง ๋ฐ์ผ๋ก ๋๊ฐ\n",
"์ ์ ๋ถ์ผ์น ์ํ: ['์ ์ ๋์กฐ: ํ์ ๋ฑ๊ธ๊ณผ ๊ด๋ฆฌ ๊ธฐ๋ก ๋ฑ๊ธ์ด 2๋จ๊ณ ์ด์ ์ฐจ์ด(์ ์ ์ด์ ์ ํธ)']\n",
"\n",
"[์
๋ฌด ํ๋ก์ธ์ค ๋งต]\n",
"1.์ดฌ์ -> 2.์ ์ ๋์กฐ -> 3.์ ์ฒ๋ฆฌ -> 4.๊ฒฐํจ ํ๋ณ -> 5.์์๋ ์ฐ์ -> 6.๊ฒ์ฌ๊ด ๋ณด์ -> 7.์ ๋น ๋ฑ๊ธ ์น์ธ -> 8.์ ์ ํ๋ฅ -> 9.์ฌํ๊ฒํ (AAR)\n",
" ์ดฌ์ : ์ฌ์ง ID, ์ดฌ์์ผ, ์ดฌ์ ์กฐ๊ฑด(์กฐ๋ช
ยท์ค์ผ)\n",
" ์ ์ ๋์กฐ : ์ฅ๋น ์๋ณ์ฝ๋, ์ ์กฐ์ฐ๋, ๊ฐ๋์๊ฐ, ๊ธฐ๋ก์ ์ ๋น ๋ฑ๊ธ\n",
" ์ ์ฒ๋ฆฌ : ์กฐ๋ช
๋ณด์ , ํด์๋ ํ์คํ, ๊ฒฐํจ ์์ญ(๋ฐ์ด๋ฉ ๋ฐ์ค)\n",
" ๊ฒฐํจ ํ๋ณ : ๊ฒฐํจ ์ ํ, ๊ฒฐํจ ๋ฉด์ , ์ถ์ ๊น์ด\n",
" ์์๋ ์ฐ์ : ์์๋ ์ ์(0~100), ํ์ ์ ๋ขฐ๋\n",
" ๊ฒ์ฌ๊ด ๋ณด์ : ์๋ ๋ณด์ ์ฌ๋ถ, ๋ณด์ ์ฌ์ \n",
" ์ ๋น ๋ฑ๊ธ ์น์ธ : ํ์ฅยท์ผ์ ยท์ฐฝ์ ๋น ๋ฑ๊ธ, ์น์ธ์, ์น์ธ ์๊ฐ\n",
" ์ ์ ํ๋ฅ : ํ์ ๊ฒฐ๊ณผ์ ๊ธฐ๋ก์ ๋ฑ๊ธ์ ๋ถ์ผ์น, ์ ์ ์์ ์์ฒญ\n",
" ์ฌํ๊ฒํ (AAR) : ์คํ ์์ธ, ํ๋ผ๋ฏธํฐ ์ฌ๋ณด์ ์ด๋ ฅ\n",
"\n",
"[๋ผ๋ฒจ๋ง ๊ธฐ์ค]\n",
" crack : ์ ํ ์ด๋์ด ๊ท ์ด, ๊ธธ์ด 10px ์ด์\n",
" corrosion : ๋ถ์ ๊ฐ์ ๋ฉด, ๋ฉด์ 12px ์ด์\n",
" damage : ํจ๋ชฐยทํ๋จ ์ค๊ณฝ(๋ฐ์ ํ
๋๋ฆฌ ํฌํจ)\n",
" leak : ์ด๋ก๊ณ ๋ฒ์ง๋ ์ก์ฒด ์๊ตญ(์๋ ๋ฐฉํฅ ํ๋ฆ)\n",
" none : ๊ฒฐํจ ์์(๋ฌดํดํ ์ผ๋ฃฉยท๊ธํ์ ๊ฒฐํจ์ผ๋ก ๋ผ๋ฒจํ์ง ์์)\n",
"\n",
"[๋ฐ์ดํฐ ๋์
๋๋ฆฌ] ํผ์ฒ 24๊ฐ (๋ชฉํ 20๊ฐ ์ด์: ๋ฌ์ฑ)\n",
" ๊ทธ๋ฃน ํ๋ ์๋ฃํ ์ค๋ช
\n",
" photo img_id str ์ฌ์ง ID\n",
" photo shot_day int ์ดฌ์์ผ(์ ๊ฒ ์์ ํ ๊ฒฝ๊ณผ์ผ)\n",
" photo width int ํ์ค ํด์๋ ๊ฐ๋ก(px)\n",
" photo height int ํ์ค ํด์๋ ์ธ๋ก(px)\n",
" photo channels int ์ฑ๋ ์(RGB)\n",
" photo brightness float ํ๊ท ๋ฐ๊ธฐ(0~1)\n",
" photo mud_ratio float ์งํยท์ค์ผ ๊ฐ๋ฆผ ๋น์จ(์ถ์ )\n",
" spec equip_id str ์ฅ๋น ์๋ณ์ฝ๋(๊ตญ๋ฐฉ ํ์ค ์ฝ๋ ํ์์ ๋ชจ์ฌํ ๊ฐ์ ์ฝ๋)\n",
" spec equip_type str ์ฅ๋น ๊ตฌ๋ถ\n",
"... (์ดํ ์๋ต)\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
02. ์ด๋ฏธ์ง ์กฐ๋ ๋ณด์ , ํด์๋ ํ์คํ ๋ฐ ๊ฒฐํจ ์์ญ(Bounding Box) ์ ์ฒ๋ฆฌ ๋ชจ๋ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์กฐ๋ ๋ณด์ , ํด์๋ ํ์คํ ๋ฐ ๊ฒฐํจ ์์ญ ๋ฐ์ด๋ฉ ๋ฐ์ค ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์กฐ๋ช
๋ณด์ , ํด์๋ ํ์คํ, ๊ฒฐํจ ์์ญ(Bounding Box) ์ ์ ๋ฅผ ํฌํจํ ์ฌ์งยท์ ์ ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **ํด์๋, ๋ฐ๊ธฐ, ํ๊ธฐ๊ฐ ์ ๊ฐ๊ฐ์ธ ๊ฐ์ ์์ฒ ์ฌ์ง๊ณผ ์ ์ยท๋ฐ์ค ๋ผ๋ฒจ์ ์ ์ ๊ท์น 10์ข
์ด์์ ์์๋๋ก ์ ์ฉ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/etl/qlt03_data_cleansing.py ๋ฐ ์ ์ ํ์ง ๋ณด๊ณ ์` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๊ตฐ์ ํน์ ์์ธ(๋น๊ณํ ์ ๋น, ๊ธด๊ธ ๋ถ์ถ ๋ฑ) ๋ฐ์ดํฐ ๋ณด์กด์ ํ๋น์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ๋ฐ์ดํฐ ์ ์ ๊ท์น 10์ข
์ ์ฉ, ๋ฐ์ดํฐ ์ ํฉ์ฑ ๊ฒ์ฆ ํต๊ณผ์จ 100%\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ETL ํ์ดํ๋ผ์ธ ๊ตฌํ ๋ฐ ์ ์ ๋ ๋ง์คํฐ ๋ฐ์ดํฐ์
๊ตฌ์ถ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ์ ํ ๋ฐ์ดํฐ ํต๊ณ๋(๊ธฐ์ ํต๊ณ) ๋น๊ต ๋ฐ ๋ฌด๊ฒฐ์ฑ ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"ํด์๋ยท๋ฐ๊ธฐยทํ๊ธฐ๊ฐ ์ ๊ฐ๊ฐ์ธ ์ฅ๋น ์ธ๊ด ์ฌ์ง๊ณผ ์ ์ ๊ธฐ๋ก, ๊ฒฐํจ ์์ญ(๋ฐ์ด๋ฉ ๋ฐ์ค) ๋ผ๋ฒจ์ ๊ฐ์ ์์ฒ ์๋ฃ์ ์กฐ๋ช
๋ณด์ ยทํด์๋ ํ์คํยท๋ฐ์ค ์ ๋ฆฌ๋ฅผ ํฌํจํ ์ ์ ๊ท์น 11์ข
์ ์ ์ฉํ๊ณ , ๊ธด๊ธ ์ ๊ฒ ์ฌ์ง ๊ฐ์ ๊ตฐ์ ์์ธ ๋ฐ์ดํฐ๋ ๋ณด์กดํ๋ฉด์ ์ ํฉ์ฑ ๊ฒ์ฆ ํต๊ณผ์จ๊ณผ ์ ์ ์ ํ ํต๊ณ๋ฅผ ํ์ธํ๋ค.\"\"\"\n",
"import re\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.impute import KNNImputer\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ฅ๋น ์ธ๊ด ์ฌ์ง ์์ฑ๊ธฐ (์ค์ ์ฅ๋นยท๋ถ๋ยท์์น ์ฌ์ง์ด ์๋๋ผ ๋์๋ก ๊ทธ๋ฆฐ 64x64 ํฉ์ฑ ์ด๋ฏธ์ง)\n",
"# ๋์ฅ๋ ๊ธ์ ํ๋ฉด์ ๊ท ์ด(crack)ยท๋ถ์(corrosion)ยทํ์(damage)ยท๋์ (leak)๋ฅผ ๊ทธ๋ฆฌ๊ณ , ์กฐ๋ช
ํธ์ฐจ์ ์งํ(mud) ์ค์ผ์ ์๋๋ค.\n",
"# ์ด๋ฏธ์ง์ ํจ๊ป ์ฅ๋น ๊ด๋ฆฌ ๊ธฐ๋ก(์ ์ ์์ฝ)๊ณผ ์ ๋ต ๊ฒฐํจ ๋ง์คํฌยท์์๋ยท์ ๋น ๋ฑ๊ธ์ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"from scipy import ndimage as ndi\n",
"\n",
"SZ = 64\n",
"CLASSES = [\"none\", \"crack\", \"corrosion\", \"damage\", \"leak\"]\n",
"KO_CLS = {\"none\": \"๊ฒฐํจ ์์\", \"crack\": \"๊ท ์ด\", \"corrosion\": \"๋ถ์\", \"damage\": \"ํ์\", \"leak\": \"๋์ \"}\n",
"CLS_W = {\"none\": 0.0, \"crack\": 1.4, \"corrosion\": 1.0, \"damage\": 1.6, \"leak\": 0.6} # ์์๋ ๊ณ์ฐ์ฉ ๊ฒฐํจ ์ ํ ๊ฐ์ค์น\n",
"GRADE_NAME = {1: \"ํ์ฅ์ ๋น\", 2: \"์ผ์ ์ ๋น\", 3: \"์ฐฝ์ ๋น\"}\n",
"YY, XX = np.mgrid[0:SZ, 0:SZ]\n",
"\n",
"\n",
"def grade_of(idx):\n",
" # [TODO : ํ์ ์ค์ต 2] ์ด๋ฏธ์ง ์กฐ๋ ๋ณด์ , ํด์๋ ํ์คํ ๋ฐ ๊ฒฐํจ ์์ญ(Bounding Box) ์ ์ฒ๋ฆฌ ๋ชจ๋ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def blob(rng, cy, cx, r, rough=0.35):\n",
" \"\"\"๋ถ๊ท์นํ ์ํ ์์ญ ๋ง์คํฌ.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 2] ์ด๋ฏธ์ง ์กฐ๋ ๋ณด์ , ํด์๋ ํ์คํ ๋ฐ ๊ฒฐํจ ์์ญ(Bounding Box) ์ ์ฒ๋ฆฌ ๋ชจ๋ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def draw(rng, cls, sev):\n",
" \"\"\"๊ฒฐํจ ์ ํยท์ฌ๊ฐ๋ sev(0~1)์ ๋ง๋ ์ ๋ต ๋ง์คํฌ์ ์ ์ ๋ณด๋ฅผ ๋ง๋ ๋ค.\"\"\"\n",
" m = np.zeros((SZ, SZ), bool)\n",
" if cls == \"crack\":\n",
" y, x, ang = rng.uniform(12, 52), rng.uniform(12, 52), rng.uniform(0, np.pi)\n",
" for _ in range(int(20 + 40 * sev)):\n",
" ang += rng.normal(0, 0.35); y += np.sin(ang) * 1.2; x += np.cos(ang) * 1.2\n",
" if 0 <= int(y) < SZ and 0 <= int(x) < SZ:\n",
" m[int(y), int(x)] = True\n",
" m = ndi.binary_dilation(m, iterations=1 + int(sev > 0.6))\n",
" elif cls == \"corrosion\":\n",
" for _ in range(int(rng.integers(1, 4))):\n",
" m |= blob(rng, rng.uniform(14, 50), rng.uniform(14, 50), 3 + 9 * sev * rng.uniform(0.6, 1.1))\n",
" elif cls == \"damage\":\n",
" m = blob(rng, rng.uniform(20, 44), rng.uniform(20, 44), 6 + 12 * sev, 0.3)\n",
" elif cls == \"leak\":\n",
" x0, y0 = rng.uniform(16, 48), rng.uniform(8, 24)\n",
" w, ln = 2 + 4 * sev, 12 + 26 * sev\n",
" m = (np.abs(XX - x0 - 0.15 * (YY - y0)) < w * (1 - 0.4 * (YY - y0) / ln)) & (YY > y0) & (YY < y0 + ln)\n",
" return m\n",
"\n",
"\n",
"def make_images(n=600, seed=0, mud=0.0, p_none=0.25, dark=False):\n",
" \"\"\"๊ฐ์ ์ธ๊ด ์ฌ์ง n์ฅ(+์งํ ์ค์ผ mud ๋น์จ)๊ณผ ๊ด๋ฆฌ ๊ธฐ๋กยท์ ๋ต. ๋ฐํ: meta(DataFrame), imgs(n,64,64,3), masks(n,64,64).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" imgs, masks, rows = np.zeros((n, SZ, SZ, 3), np.float32), np.zeros((n, SZ, SZ), bool), []\n",
" for i in range(n):\n",
" cls = \"none\" if rng.random() < p_none else str(rng.choice(CLASSES[1:], p=[0.25, 0.3, 0.25, 0.2]))\n",
" sev = 0.0 if cls == \"none\" else float(rng.beta(1.6, 2.0))\n",
" base = np.array([0.42, 0.46, 0.40]) * (1 + 0.5 * ndi.gaussian_filter(rng.normal(size=(SZ, SZ)), 8)[..., None] * 3)\n",
" img = base + rng.normal(0, 0.012, (SZ, SZ, 3))\n",
" m = draw(rng, cls, sev)\n",
" a = ndi.gaussian_filter(m.astype(float), 0.8)[..., None] # ๊ฒฝ๊ณ๋ฅผ ๋ถ๋๋ฝ๊ฒ ์๋๋ค\n",
" if cls == \"crack\":\n",
" col = img * rng.uniform(0.2, 0.45)\n",
" elif cls == \"corrosion\":\n",
" col = np.array([0.55, 0.30, 0.12]) * rng.uniform(0.75, 1.15) * (0.85 + 0.3 * rng.random((SZ, SZ, 1)))\n",
" elif cls == \"damage\":\n",
" col = np.ones_like(img) * np.array([0.34, 0.34, 0.36]) * (1.0 - 0.45 * sev) * rng.uniform(0.75, 1.0)\n",
" rim = m & ~ndi.binary_erosion(m, iterations=2)\n",
" a = np.maximum(a, 0.0); img = np.where(rim[..., None], 0.7, img)\n",
" elif cls == \"leak\":\n",
" col = np.ones_like(img) * np.array([0.10, 0.09, 0.08]) * rng.uniform(0.8, 2.6)\n",
" else:\n",
" col = img\n",
" img = img * (1 - a) + col * a\n",
" if rng.random() < 0.35: # ๋ฌดํดํ ์ผ๋ฃฉ(๊ฒฐํจ ์๋): ์
์ ๊ฐ์ ๋ฒ์ง\n",
" st = blob(rng, rng.uniform(5, 58), rng.uniform(5, 58), rng.uniform(6, 14), 0.5)\n",
" al = ndi.gaussian_filter(st.astype(float), 2.5)[..., None] * rng.uniform(0.15, 0.4)\n",
" img = img * (1 - al) + np.array([0.50, 0.40, 0.30]) * al\n",
" if rng.random() < 0.3: # ๋ฌดํดํ ๊ธด ์คํฌ๋์น(๋ฐ์ ์ )\n",
" y0, x0, ang = rng.uniform(5, 58), rng.uniform(5, 58), rng.uniform(0, np.pi)\n",
" for t in np.linspace(0, 25, 40):\n",
" yy, xx = int(y0 + t * np.sin(ang)), int(x0 + t * np.cos(ang))\n",
" if 0 <= yy < SZ and 0 <= xx < SZ:\n",
" img[yy, xx] = np.clip(img[yy, xx] * 1.5, 0, 1)\n",
" if mud > 0: # ์งํ ์ค์ผ: ๊ฒฐํจ ์๋ฅผ ๋ฎ์ ์ ์๋ค(์ ๋ต ๋ง์คํฌ๋ ๊ทธ๋๋ก)\n",
" mm = np.zeros((SZ, SZ), bool)\n",
" while mm.mean() < mud:\n",
" mm |= blob(rng, rng.uniform(0, SZ), rng.uniform(0, SZ), rng.uniform(5, 11), 0.5)\n",
" img[mm] = np.array([0.38, 0.30, 0.22]) * (0.85 + 0.3 * rng.random((mm.sum(), 1)))\n",
" gain = rng.uniform(0.3, 0.55) if dark else rng.uniform(0.5, 1.15)\n",
" ang = rng.uniform(0, 2 * np.pi)\n",
" light = gain * (1 + 0.3 * (np.cos(ang) * (XX - 32) + np.sin(ang) * (YY - 32)) / 32)\n",
" imgs[i] = np.clip(img * light[..., None] + rng.normal(0, 0.025, (SZ, SZ, 3)), 0, 1)\n",
" masks[i] = m\n",
" area = float(m.mean())\n",
" depth = {\"none\": 0.0, \"crack\": 0.3 + 3.0 * sev, \"corrosion\": 0.1 + 1.5 * sev, \"damage\": 1.0 + 8.0 * sev, \"leak\": 0.0}[cls] * rng.lognormal(0, 0.1)\n",
" idx = float(np.clip(100 * (1 - np.exp(-(CLS_W[cls] * area * 9 + 0.05 * depth))) + rng.normal(0, 1.5), 0, 100))\n",
" rows.append({\"img_id\": f\"IMG{seed:02d}{i:04d}\", \"cls\": cls, \"sev\": sev, \"area_frac\": area, \"depth_mm\": depth, \"damage_idx\": idx,\n",
" \"grade\": int(grade_of(idx)), \"gain\": gain, \"mud\": mud})\n",
" meta = pd.DataFrame(rows)\n",
" # ์ฅ๋น ๊ด๋ฆฌ ๊ธฐ๋ก(์ ์ ์์ฝ): ์ฌ์ง ํ์ ๊ณผ ๋์กฐํ ๊ฐ\n",
" age = rng.integers(1, 25, n)\n",
" meta[\"day\"] = np.sort(rng.integers(0, 90, n))\n",
" meta[\"equip_type\"] = rng.choice([\"vehicle\", \"generator\", \"comm\", \"trailer\"], n, p=[0.4, 0.2, 0.2, 0.2])\n",
" meta[\"mfg_year\"] = 2026 - age\n",
" meta[\"op_hours\"] = np.clip(rng.normal(180 * age, 400, n) + 8 * meta[\"damage_idx\"], 10, 20000)\n",
" meta[\"env\"] = rng.choice([\"coastal\", \"inland\"], n, p=[0.3, 0.7])\n",
" reg = np.clip(meta[\"grade\"] + rng.choice([-1, 0, 0, 0, 0, 0, 1, 2], n), 1, 3) # ์ฅ๋น ๊ด๋ฆฌ ๊ธฐ๋ก์ ์ง์ ์ ๋น ๋ฑ๊ธ\n",
" meta[\"reg_grade\"] = reg\n",
" meta[\"emergency\"] = rng.random(n) < 0.04 # ๊ธด๊ธ ์ ๊ฒ ์์ฒญ(ํ์ง ๋ถ๋์ด์ด๋ ๋ณด์กด)\n",
" return meta, imgs, masks\n",
"\n",
"# ---- ์ ์ฒ๋ฆฌ(์กฐ๋ช
๋ณด์ ยทํ์คํ)์ ๊ฒฐํจ ํ๋ณด ๋ถํ , ํน์ง ์ถ์ถ (cv2 ๋์ numpyยทscipy ์ฌ์ฉ) ----\n",
"def correct(img):\n",
" \"\"\"์กฐ๋ช
๋ณด์ : ๋ฐ๊ธฐ์ ํ๋ฉด(์กฐ๋ช
๊ธฐ์ธ๊ธฐ)์ ์ ํฉํด ๋๋๋ค. ์ด๋์ด ๊ฒฐํจ ํ์๋ ์ ์ธํ๊ณ ๋ ๋ฒ ๋ฐ๋ณต ์ ํฉํ๋ค. ๋ฐํ: ๋ณด์ ๋ฐ๊ธฐ(์ ์ ๋ฉด ์ฝ 1), ์ ๋น์จ.\"\"\"\n",
" g = img.mean(axis=2)\n",
" A = np.c_[np.ones(SZ * SZ), XX.ravel() / SZ, YY.ravel() / SZ]\n",
" keep = np.ones(SZ * SZ, bool)\n",
" for _ in range(3):\n",
" coef, *_ = np.linalg.lstsq(A[keep], g.ravel()[keep], rcond=None)\n",
" plane = (A @ coef).reshape(SZ, SZ)\n",
" keep = (np.abs(g - plane) < 0.08 * np.maximum(plane, 1e-3)).ravel()\n",
" if keep.sum() < 200:\n",
" keep[:] = True\n",
" c = g / np.maximum(plane, 1e-3)\n",
" chroma = img[..., 0] / (img.sum(axis=2) + 1e-6)\n",
" return c, chroma\n",
"\n",
"\n",
"SEGP = {\"dark\": 0.7, \"rust\": 0.5, \"rim\": 1.45} # ๋ถํ ์๊ณ๊ฐ(์ด๋์ ๋ณด์ ๋ฐ๊ธฐ, ๋ถ์ ๋น์จ, ๋ฐ์ ํ
๋๋ฆฌ): ์ฌ๋ณด์ ๋์\n",
"\n",
"\n",
"def segment(img):\n",
" \"\"\"๊ฒฐํจ ํ๋ณด ๋ง์คํฌ: ์ด๋์ด ์์ญ(๊ท ์ดยท๋์ ยทํ์) + ๋ถ์ ๊ฐ์ ์์ญ(๋ถ์) + ๋ฐ์ ํ
๋๋ฆฌ(ํ์).\"\"\"\n",
" c, chroma = correct(img)\n",
" dark = c < SEGP[\"dark\"]\n",
" rust = (chroma > SEGP[\"rust\"]) & (c > 0.45)\n",
" rim = c > SEGP[\"rim\"]\n",
" m = dark | rust | ndi.binary_dilation(rim, iterations=1) & ndi.binary_dilation(dark, iterations=3)\n",
" m = ndi.binary_opening(m, structure=np.ones((2, 2))) if m.sum() > 12 else m\n",
" return m, c, chroma, dark, rust, rim\n",
"\n",
"\n",
"def extract(img):\n",
" \"\"\"๋ถํ ๊ฒฐ๊ณผ์ ์ยท๋ชจ์์์ 24๊ฐ ํน์ง์ ๋ง๋ ๋ค.\"\"\"\n",
" m, c, chroma, dark, rust, rim = segment(img)\n",
" lab, n = ndi.label(m)\n",
" area = float(m.sum())\n",
" if n:\n",
" sizes = ndi.sum(m, lab, range(1, n + 1))\n",
" k = int(np.argmax(sizes)) + 1\n",
" sl = ndi.find_objects(lab)[k - 1]\n",
" h, w = sl[0].stop - sl[0].start, sl[1].stop - sl[1].start\n",
" big = float(sizes.max())\n",
" elong, fill = max(h, w) / max(min(h, w), 1), big / (h * w)\n",
" else:\n",
" big, elong, fill = 0.0, 1.0, 0.0\n",
" gy, gx = np.gradient(c)\n",
" mag = np.hypot(gx, gy)\n",
" inside = m.sum() > 0\n",
" return {\"area_px\": area, \"area_frac\": area / SZ ** 2, \"n_comp\": float(n), \"big_px\": big, \"elong\": elong, \"fill\": fill,\n",
" \"dark_frac\": float(dark.mean()), \"rust_frac\": float(rust.mean()), \"rim_frac\": float(rim.mean()),\n",
" \"c_mean\": float(c.mean()), \"c_std\": float(c.std()), \"c_min\": float(c.min()), \"edge_mean\": float(mag.mean()), \"edge_p95\": float(np.quantile(mag, 0.95)),\n",
" \"chroma_mean\": float(chroma.mean()), \"chroma_max\": float(chroma.max()),\n",
" \"in_dark\": float(c[m].mean()) if inside else 1.0, \"in_chroma\": float(chroma[m].mean()) if inside else float(chroma.mean()),\n",
" \"ring_contrast\": float(c[ndi.binary_dilation(m, iterations=3) & ~m].mean() - c[m].mean()) if inside and (~m).any() else 0.0,\n",
" \"thin\": float(1 - fill), \"gain\": float(img.mean()), \"sat\": float((img.max(axis=2) - img.min(axis=2)).mean()),\n",
" \"cy\": float(ndi.center_of_mass(m)[0] / SZ) if inside else 0.5, \"cx\": float(ndi.center_of_mass(m)[1] / SZ) if inside else 0.5}\n",
"\n",
"\n",
"def feature_table(imgs):\n",
" return pd.DataFrame([extract(im) for im in imgs])\n",
"\n",
"rng = np.random.default_rng(2)\n",
"meta, imgs, masks = make_images(150, seed=2)\n",
"n0 = len(meta)\n",
"applied = []\n",
"\n",
"\n",
"def rule(name, n):\n",
" applied.append((name, int(n)))\n",
"\n",
"\n",
"def true_box(m):\n",
" ys, xs = np.where(m)\n",
" return (int(xs.min()), int(ys.min()), int(xs.max() - xs.min() + 1), int(ys.max() - ys.min() + 1)) if len(xs) else None\n",
"\n",
"\n",
"# ---------------- 1) ์์ฒ ์๋ฃ ๋ง๋ค๊ธฐ(์ผ๋ถ๋ฌ ๋ฃ์ ์ค๋ฅ) ----------------\n",
"raw_imgs, rows = [], []\n",
"sizes = rng.choice([32, 64, 128], n0, p=[0.2, 0.55, 0.25])\n",
"dark_set = set(rng.choice(n0, 35, replace=False))\n",
"emerg = set(rng.choice(n0, 6, replace=False))\n",
"for i in range(n0):\n",
" im = imgs[i]\n",
" if sizes[i] != 64:\n",
" im = np.clip(ndi.zoom(im, (sizes[i] / 64, sizes[i] / 64, 1), order=1), 0, 1)\n",
" if i in dark_set or i in emerg:\n",
" im = im * 0.45 # ์ ์กฐ๋ ์ดฌ์\n",
" raw_imgs.append(im.astype(np.float32))\n",
" tb = true_box(masks[i]); k = sizes[i] / 64\n",
" box = None if tb is None else [tb[0] * k, tb[1] * k, tb[2] * k, tb[3] * k]\n",
" fmt = \"xywh\"\n",
" if box is not None and rng.random() < 0.4: # ์ผ๋ถ๋ xyxy ํ์์ผ๋ก ๊ธฐ๋ก\n",
" box, fmt = [box[0], box[1], box[0] + box[2], box[1] + box[3]], \"xyxy\"\n",
" rows.append({\"img_id\": f\"IMG{i:06d}\", \"shot\": f\"2026-{rng.integers(1, 10):02d}-{rng.integers(1, 28):02d}\", \"equip_id\": f\"VX-{1000 + i:04d}-{2000 + i:04d}\",\n",
" \"mfg_year\": int(meta.loc[i, \"mfg_year\"]), \"op_hours\": float(meta.loc[i, \"op_hours\"]), \"hours_unit\": \"h\", \"reg_grade\": float(meta.loc[i, \"reg_grade\"]),\n",
" \"w\": int(sizes[i]), \"box\": box, \"box_fmt\": fmt, \"emergency\": i in emerg})\n",
"raw = pd.DataFrame(rows)\n",
"idx = rng.choice(n0, 60, replace=False)\n",
"a, b, c, d, e, f, g, h = idx[:8], idx[8:16], idx[16:24], idx[24:32], idx[32:40], idx[40:46], idx[46:52], idx[52:60]\n",
"raw.loc[a, \"img_id\"] = raw.loc[a, \"img_id\"].str.replace(\"IMG\", \"img-\") # ์ฌ์ง ID ํ๊ธฐ ๋ถ์ผ์น\n",
"raw.loc[b, \"shot\"] = [x.replace(\"-\", \"\") for x in raw.loc[b, \"shot\"]] # ์ดฌ์์ผ ํ์ ํผ์ฌ\n",
"raw.loc[c, \"op_hours\"] = raw.loc[c, \"op_hours\"] / 24; raw.loc[c, \"hours_unit\"] = \"day\" # ๊ฐ๋์๊ฐ ๋จ์ ํผ์ฌ\n",
"raw.loc[d, \"mfg_year\"] = rng.choice([1900, 2085], len(d)) # ๋ถ๊ฐ๋ฅํ ์ ์กฐ์ฐ๋\n",
"raw.loc[e, [\"mfg_year\", \"op_hours\", \"reg_grade\"]] = np.nan # ์ ์ ๊ฒฐ์ธก\n",
"for i in f: # ๋ฐ์ค๊ฐ ์ฌ์ง ๋ฐ์ผ๋ก ๋๊ฐ\n",
" if raw.at[i, \"box\"] is not None and raw.at[i, \"box_fmt\"] == \"xywh\":\n",
" raw.at[i, \"box\"] = [raw.at[i, \"w\"] - 2, raw.at[i, \"box\"][1], raw.at[i, \"box\"][2] + 6, raw.at[i, \"box\"][3]]\n",
"for i in g: # ํญ 0 ๋ฐ์ค\n",
" if raw.at[i, \"box\"] is not None and raw.at[i, \"box_fmt\"] == \"xywh\":\n",
" raw.at[i, \"box\"] = [raw.at[i, \"box\"][0], raw.at[i, \"box\"][1], 0, 0]\n",
"dup_rows = raw.sample(8, random_state=1)\n",
"raw = pd.concat([raw, dup_rows], ignore_index=True)\n",
"raw_imgs += [raw_imgs[int(r[\"img_id\"][-6:])] for _, r in dup_rows.iterrows()] # ์ค๋ณต ์ฌ์ง\n",
"print(f\"์์ฒ ์ฌ์ง {len(raw)}์ฅ (์ค๋ณต 8์ฅ, ์ค๋ฅ ์ฃผ์
{len(idx)}๊ฑด, ์ ์กฐ๋ {len(dark_set | emerg)}์ฅ, ๊ธด๊ธ ์ ๊ฒ {len(emerg)}์ฅ ํฌํจ)\")\n",
"\n",
"# ---------------- 2) ์ ์ ๊ท์น 11์ข
----------------\n",
"df, ims = raw.copy(), list(raw_imgs)\n",
"dup = df.duplicated([\"img_id\", \"equip_id\", \"shot\"]); rule(\"R1 ์ค๋ณต ์ฌ์ง ์ ๊ฑฐ\", dup.sum())\n",
"keep = (~dup).values; df = df[keep].reset_index(drop=True); ims = [im for im, k in zip(ims, keep) if k]\n",
"bad_id = ~df[\"img_id\"].str.match(r\"^IMG\\d{6}$\"); df[\"img_id\"] = \"IMG\" + df[\"img_id\"].str.extract(r\"(\\d{6})\")[0]; rule(\"R2 ์ฌ์ง ID ํ๊ธฐ ํต์ผ\", bad_id.sum())\n",
"t1 = pd.to_datetime(df[\"shot\"], format=\"%Y-%m-%d\", errors=\"coerce\"); t2 = pd.to_datetime(df[\"shot\"], format=\"%Y%m%d\", errors=\"coerce\")\n",
"rule(\"R3 ์ดฌ์์ผ ํ์ ํต์ผ\", t1.isna().sum()); df[\"shot\"] = t1.fillna(t2)\n",
"day = df[\"hours_unit\"] == \"day\"; df.loc[day, \"op_hours\"] *= 24; df[\"hours_unit\"] = \"h\"; rule(\"R4 ๊ฐ๋์๊ฐ ๋จ์ ํต์ผ(์ผ -> ์๊ฐ)\", day.sum())\n",
"bad = ~df[\"mfg_year\"].between(1990, 2026) & df[\"mfg_year\"].notna(); df.loc[bad, \"mfg_year\"] = np.nan; rule(\"R5 ๋ถ๊ฐ๋ฅ ์ ์กฐ์ฐ๋ ๊ฒฐ์ธก ์ฒ๋ฆฌ\", bad.sum())\n",
"nres = sum(im.shape[0] != 64 for im in ims)\n",
"ims = [np.clip(ndi.zoom(im, (64 / im.shape[0], 64 / im.shape[1], 1), order=1), 0, 1) if im.shape[0] != 64 else im for im in ims]\n",
"rule(\"R6 ํด์๋ ํ์คํ(32/128 -> 64px)\", nres)\n",
"before_b = np.array([im.mean() for im in ims])\n",
"fixed = []\n",
"for im in ims:\n",
" cc, _ = correct(im) # ๋ณด์ ๋ฐ๊ธฐ = ๋ฐ๊ธฐ / ์กฐ๋ช
ํ๋ฉด\n",
" plane = im.mean(axis=2) / np.maximum(cc, 1e-6)\n",
" fixed.append(np.clip(im * 0.45 / np.maximum(plane, 1e-3)[..., None], 0, 1)) # ์กฐ๋ช
๊ธฐ์ธ๊ธฐ๋ฅผ ํด๊ณ ์ ์ ๋ฉด ๋ฐ๊ธฐ๋ฅผ 0.45๋ก ๋ง์ถ๋ค\n",
"ims = fixed; rule(\"R7 ์กฐ๋ช
๋ณด์ ยท์ ๊ทํ(ํ๊ท ๋ฐ๊ธฐ 0.3 ๋ฏธ๋ง ์ฌ์ง ์)\", (before_b < 0.3).sum())\n",
"bx = []\n",
"nx = 0\n",
"for (_, r) in df.iterrows():\n",
" if r[\"box\"] is None:\n",
" bx.append(None); continue\n",
" x, y, w_, h_ = r[\"box\"]\n",
" if r[\"box_fmt\"] == \"xyxy\":\n",
" w_, h_ = w_ - x, h_ - y; nx += 1\n",
" k = 64 / r[\"w\"]\n",
" bx.append([x * k, y * k, w_ * k, h_ * k])\n",
"rule(\"R8 ๋ฐ์ค ํ์ ํต์ผ(xyxy -> xywh, 64px ์ขํ)\", nx)\n",
"clip_n, zero_n, boxes, manual = 0, 0, [], []\n",
"for i, b_ in enumerate(bx):\n",
" if b_ is None:\n",
" boxes.append(None); continue\n",
" x, y, w_, h_ = b_\n",
" x2, y2 = min(x + w_, 64), min(y + h_, 64)\n",
" if x2 > 64 or x + w_ > 64.5 or y + h_ > 64.5:\n",
" clip_n += 1\n",
" if x2 - x < 1 or y2 - y < 1:\n",
" zero_n += 1; manual.append(df.loc[i, \"img_id\"]); boxes.append(None); continue\n",
" boxes.append([int(round(x)), int(round(y)), int(round(x2 - x)), int(round(y2 - y))])\n",
"rule(\"R9 ๋ฐ์ค ์ขํ ํด๋ฆฌํยท๋ฌดํจ ๋ฐ์ค ์๋ ํ์ธ ๋ถ๋ฆฌ\", clip_n + zero_n)\n",
"miss = df[[\"mfg_year\", \"op_hours\", \"reg_grade\"]].isna().sum().sum()\n",
"df[[\"mfg_year\", \"op_hours\", \"reg_grade\"]] = KNNImputer(n_neighbors=5).fit_transform(df[[\"mfg_year\", \"op_hours\", \"reg_grade\", \"w\"]])[:, :3]\n",
"df[\"mfg_year\"] = df[\"mfg_year\"].round().astype(int); df[\"reg_grade\"] = df[\"reg_grade\"].round().astype(int); rule(\"R10 ์ ์ ๊ฒฐ์ธก KNN ๋ณด์ \", miss)\n",
"df[\"quality_flag\"] = np.array([im.std() < 0.03 for im in ims]) & ~df[\"emergency\"]\n",
"rule(\"R11 ๊ธด๊ธ ์ ๊ฒ ์ฌ์ง ๋ณด์กดยท์ผ๋ฐ ์ ํ์ง ํ์\", int(df[\"emergency\"].sum()))\n",
"\n",
"# ---------------- 3) ์ ํฉ์ฑ ๊ฒ์ฆ ----------------\n",
"after_b = np.array([im.mean() for im in ims])\n",
"tb = {r[\"img_id\"]: true_box(masks[int(r[\"img_id\"][-6:])]) for _, r in df.iterrows()}\n",
"ious = []\n",
"for (_, r), bb in zip(df.iterrows(), boxes):\n",
" t = tb[r[\"img_id\"]]\n",
" if bb is not None and t is not None:\n",
" ix = max(0, min(bb[0] + bb[2], t[0] + t[2]) - max(bb[0], t[0])); iy = max(0, min(bb[1] + bb[3], t[1] + t[3]) - max(bb[1], t[1]))\n",
" ious.append(ix * iy / (bb[2] * bb[3] + t[2] * t[3] - ix * iy))\n",
"chk = {\"์ค๋ณต ์ฌ์ง 0์ฅ\": not df[\"img_id\"].duplicated().any(), \"์ฌ์ง ID ํ์\": df[\"img_id\"].str.match(r\"^IMG\\d{6}$\").all(), \"์ดฌ์์ผ ๋ณํ\": df[\"shot\"].notna().all(),\n",
" \"๊ฐ๋์๊ฐ ๋จ์ h\": (df[\"hours_unit\"] == \"h\").all(), \"์ ์กฐ์ฐ๋ 1990~2026\": df[\"mfg_year\"].between(1990, 2026).all(),\n",
" \"๋ชจ๋ ์ฌ์ง 64x64x3\": all(im.shape == (64, 64, 3) for im in ims), \"๋ฐ๊ธฐ ํธ์ฐจ ๊ฐ์\": after_b.std() < before_b.std() / 2,\n",
" \"๋ฐ์ค๊ฐ ์ฌ์ง ์\": all(b_ is None or (b_[0] + b_[2] <= 64 and b_[1] + b_[3] <= 64) for b_ in boxes), \"๋ฐ์ค ๋ฉด์ ์์\": all(b_ is None or (b_[2] > 0 and b_[3] > 0) for b_ in boxes),\n",
" \"์ ์ ๊ฒฐ์ธก 0๊ฑด\": df[[\"mfg_year\", \"op_hours\", \"reg_grade\"]].notna().all().all(), f\"๊ธด๊ธ ์ ๊ฒ {int(df['emergency'].sum())}์ฅ ๋ณด์กด\": int(df[\"emergency\"].sum()) == len(emerg),\n",
" \"์ ์ ๋ฐ์ค์ ์ ๋ต ๋ฐ์ค ํ๊ท IoU 0.9 ์ด์\": float(np.mean(ious)) >= 0.9}\n",
"rate = np.mean(list(chk.values()))\n",
"print(\"\\n[์ ์ฉ ๊ท์น๊ณผ ์ฒ๋ฆฌ ๊ฑด์]\")\n",
"for nme, n in applied:\n",
" print(f\" {nme:44s} {n:4d}๊ฑด\")\n",
"print(f\"\\n์ ํฉ์ฑ ๊ฒ์ฆ {sum(chk.values())}/{len(chk)} ํต๊ณผ, ํต๊ณผ์จ {rate:.0%} (๋ชฉํ 100%: {'๋ฌ์ฑ' if rate == 1 else '๋ฏธ๋ฌ'}), ๊ท์น {len(applied)}์ข
(๋ชฉํ 10์ข
์ด์: {'๋ฌ์ฑ' if len(applied) >= 10 else '๋ฏธ๋ฌ'})\")\n",
"for k, v in chk.items():\n",
" if not v:\n",
" print(\" ์คํจ:\", k, \"(IoU %.2f)\" % np.mean(ious))\n",
"print(f\"์ ์ ํ ์ฌ์ง {len(df)}์ฅ, ๋ฐ์ค ์๋ ํ์ธ {len(manual)}์ฅ, ๊ธด๊ธ ์ ๊ฒ {int(df['emergency'].sum())}์ฅ ์ ์ง, ์ ์ ๋ฐ์ค ํ๊ท IoU {np.mean(ious):.2f}\")\n",
"cmp = pd.DataFrame({\"์ ์ ์ \": [before_b.mean(), before_b.std(), raw[\"op_hours\"].mean(), raw[\"mfg_year\"].min(), raw[\"mfg_year\"].max()],\n",
" \"์ ์ ํ\": [after_b.mean(), after_b.std(), df[\"op_hours\"].mean(), df[\"mfg_year\"].min(), df[\"mfg_year\"].max()]},\n",
" index=[\"ํ๊ท ๋ฐ๊ธฐ\", \"๋ฐ๊ธฐ ํ์คํธ์ฐจ\", \"ํ๊ท ๊ฐ๋์๊ฐ(h)\", \"์ต์ ์ ์กฐ์ฐ๋\", \"์ต๋ ์ ์กฐ์ฐ๋\"]).round(3)\n",
"print(\"\\n[์ ์ ์ ํ ํต๊ณ]\"); print(cmp.to_string())\n",
"\n",
"fig, ax = plt.subplots(1, 3, figsize=(12, 3.5))\n",
"ax[0].hist(before_b, bins=25, alpha=0.6, label=\"raw\"); ax[0].hist(after_b, bins=25, alpha=0.6, label=\"normalized\"); ax[0].set_title(\"Mean brightness\"); ax[0].legend()\n",
"j = int(np.argmin(before_b))\n",
"ax[1].imshow(raw_imgs[j][..., :] / max(raw_imgs[j].max(), 1e-3)); ax[1].set_title(\"dark photo (raw, stretched)\"); ax[1].axis(\"off\")\n",
"ax[2].imshow(ims[j]); ax[2].set_title(\"after normalization\"); ax[2].axis(\"off\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
02 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"์์ฒ ์ฌ์ง 158์ฅ (์ค๋ณต 8์ฅ, ์ค๋ฅ ์ฃผ์
60๊ฑด, ์ ์กฐ๋ 40์ฅ, ๊ธด๊ธ ์ ๊ฒ 6์ฅ ํฌํจ)\n",
"\n",
"[์ ์ฉ ๊ท์น๊ณผ ์ฒ๋ฆฌ ๊ฑด์]\n",
" R1 ์ค๋ณต ์ฌ์ง ์ ๊ฑฐ 8๊ฑด\n",
" R2 ์ฌ์ง ID ํ๊ธฐ ํต์ผ 8๊ฑด\n",
" R3 ์ดฌ์์ผ ํ์ ํต์ผ 8๊ฑด\n",
" R4 ๊ฐ๋์๊ฐ ๋จ์ ํต์ผ(์ผ -> ์๊ฐ) 8๊ฑด\n",
" R5 ๋ถ๊ฐ๋ฅ ์ ์กฐ์ฐ๋ ๊ฒฐ์ธก ์ฒ๋ฆฌ 8๊ฑด\n",
" R6 ํด์๋ ํ์คํ(32/128 -> 64px) 71๊ฑด\n",
" R7 ์กฐ๋ช
๋ณด์ ยท์ ๊ทํ(ํ๊ท ๋ฐ๊ธฐ 0.3 ๋ฏธ๋ง ์ฌ์ง ์) 74๊ฑด\n",
" R8 ๋ฐ์ค ํ์ ํต์ผ(xyxy -> xywh, 64px ์ขํ) 54๊ฑด\n",
" R9 ๋ฐ์ค ์ขํ ํด๋ฆฌํยท๋ฌดํจ ๋ฐ์ค ์๋ ํ์ธ ๋ถ๋ฆฌ 4๊ฑด\n",
" R10 ์ ์ ๊ฒฐ์ธก KNN ๋ณด์ 32๊ฑด\n",
" R11 ๊ธด๊ธ ์ ๊ฒ ์ฌ์ง ๋ณด์กดยท์ผ๋ฐ ์ ํ์ง ํ์ 6๊ฑด\n",
"\n",
"์ ํฉ์ฑ ๊ฒ์ฆ 12/12 ํต๊ณผ, ํต๊ณผ์จ 100% (๋ชฉํ 100%: ๋ฌ์ฑ), ๊ท์น 11์ข
(๋ชฉํ 10์ข
์ด์: ๋ฌ์ฑ)\n",
"์ ์ ํ ์ฌ์ง 150์ฅ, ๋ฐ์ค ์๋ ํ์ธ 3์ฅ, ๊ธด๊ธ ์ ๊ฒ 6์ฅ ์ ์ง, ์ ์ ๋ฐ์ค ํ๊ท IoU 0.99\n",
"\n",
"[์ ์ ์ ํ ํต๊ณ]\n",
" ์ ์ ์ ์ ์ ํ\n",
"ํ๊ท ๋ฐ๊ธฐ 0.293 0.447\n",
"๋ฐ๊ธฐ ํ์คํธ์ฐจ 0.117 0.005\n",
"ํ๊ท ๊ฐ๋์๊ฐ(h) 2494.475 2609.453\n",
"์ต์ ์ ์กฐ์ฐ๋ 1900.000 2002.000\n",
"์ต๋ ์ ์กฐ์ฐ๋ 2085.000 2025.000\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 ๋น์ ๋ชจ๋ธ์ ๊ตฌํํ๊ณ EDA๋ก ํ๊ณ์ ํต์ฌ ๊ด๋ฆฌ ๋์์ ์ฐพ๋๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๋ฐ๊ธฐ ํ์คํ ๊ทธ๋จ๊ณผ ์ฑ๋ ํ๊ท ์ผ๋ก ๋ก์ง์คํฑ ํ๊ท Baseline์ ํ์ตํด ์ ํ๋ยท์ฌํ์จ์ ์ธก์ ํ๊ณ , ์์ ๊ฒฐํจ๊ณผ ์ ์กฐ๋ ์ฌ์ง์์์ ํ๊ณ ํ์ธํ๊ณ ์ ํจ์ฑ์ ์ ๊ฒํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `notebooks/qlt03_eda_analysis.ipynb ๋ฐ EDA ์์ฝ ๋ฆฌํฌํธ` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๊ตฐ์ ์์ฌ๊ฒฐ์ ์ ์ง๊ด์ ์ธ ์ธ์ฌ์ดํธ ์ ๊ณต ์์ค\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: EDA ์๊ฐํ ์ฐจํธ 8์ข
์ด์ ์์ฑ, ์ ์๋ฏธํ ์๊ด๊ด๊ณ ํผ์ฒ 5๊ฐ ์ด์ ๋์ถ\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: EDA ๋
ธํธ๋ถ ์คํ ์๋ฃ ๋ฐ ํต์ฌ ๋ฐ๊ฒฌ์ (Key Findings) ๋ฌธ์ํ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ํต๊ณ์ ๊ฐ์ค ๊ฒ์ (p-value, ์๊ด๊ณ์)์ ํตํ ์ ์์ฑ ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"๋ฐ๊ธฐ ํ์คํ ๊ทธ๋จ๊ณผ ํ๊ท ์๋ง ์ฐ๋ ์ธ๊ด ๊ฒฐํจ ์ ๋ฌด 1์ฐจ ํ๋ณ Baseline ๋น์ ๋ชจ๋ธ(๋ก์ง์คํฑ ํ๊ท)์ ๊ตฌํํด ์ฑ๋ฅ๊ณผ ํ๊ณ(์์ ๊ท ์ดยท์กฐ๋ช
์์กด)๋ฅผ ํ์ธํ๊ณ , ํ์์ ๋ถ์(EDA) ์ฐจํธ 8์ข
ยท์๊ด๋ถ์(p-value)ยท์ ๋น ๊ณต์ ํ๋ ํ (ABC)ยท์ ์ ๊ธฐ๋ก๊ณผ์ ๋ถ์ผ์น ๋ถ์์ผ๋ก ํต์ฌ ๊ด๋ฆฌ ๋์์ ์ฐพ๋๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy import stats\n",
"from sklearn.linear_model import LogisticRegression\n",
"from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score\n",
"from sklearn.preprocessing import StandardScaler\n",
"from sklearn.pipeline import make_pipeline\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ฅ๋น ์ธ๊ด ์ฌ์ง ์์ฑ๊ธฐ (์ค์ ์ฅ๋นยท๋ถ๋ยท์์น ์ฌ์ง์ด ์๋๋ผ ๋์๋ก ๊ทธ๋ฆฐ 64x64 ํฉ์ฑ ์ด๋ฏธ์ง)\n",
"# ๋์ฅ๋ ๊ธ์ ํ๋ฉด์ ๊ท ์ด(crack)ยท๋ถ์(corrosion)ยทํ์(damage)ยท๋์ (leak)๋ฅผ ๊ทธ๋ฆฌ๊ณ , ์กฐ๋ช
ํธ์ฐจ์ ์งํ(mud) ์ค์ผ์ ์๋๋ค.\n",
"# ์ด๋ฏธ์ง์ ํจ๊ป ์ฅ๋น ๊ด๋ฆฌ ๊ธฐ๋ก(์ ์ ์์ฝ)๊ณผ ์ ๋ต ๊ฒฐํจ ๋ง์คํฌยท์์๋ยท์ ๋น ๋ฑ๊ธ์ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"from scipy import ndimage as ndi\n",
"\n",
"SZ = 64\n",
"CLASSES = [\"none\", \"crack\", \"corrosion\", \"damage\", \"leak\"]\n",
"KO_CLS = {\"none\": \"๊ฒฐํจ ์์\", \"crack\": \"๊ท ์ด\", \"corrosion\": \"๋ถ์\", \"damage\": \"ํ์\", \"leak\": \"๋์ \"}\n",
"CLS_W = {\"none\": 0.0, \"crack\": 1.4, \"corrosion\": 1.0, \"damage\": 1.6, \"leak\": 0.6} # ์์๋ ๊ณ์ฐ์ฉ ๊ฒฐํจ ์ ํ ๊ฐ์ค์น\n",
"GRADE_NAME = {1: \"ํ์ฅ์ ๋น\", 2: \"์ผ์ ์ ๋น\", 3: \"์ฐฝ์ ๋น\"}\n",
"YY, XX = np.mgrid[0:SZ, 0:SZ]\n",
"\n",
"\n",
"def grade_of(idx):\n",
" # [TODO : ํ์ ์ค์ต 3] ์ธ๊ด ๊ฒฐํจ ์ ๋ฌด 1์ฐจ ํ๋ณ Baseline ๋น์ ๋ชจ๋ธ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def blob(rng, cy, cx, r, rough=0.35):\n",
" \"\"\"๋ถ๊ท์นํ ์ํ ์์ญ ๋ง์คํฌ.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 3] ์ธ๊ด ๊ฒฐํจ ์ ๋ฌด 1์ฐจ ํ๋ณ Baseline ๋น์ ๋ชจ๋ธ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def draw(rng, cls, sev):\n",
" \"\"\"๊ฒฐํจ ์ ํยท์ฌ๊ฐ๋ sev(0~1)์ ๋ง๋ ์ ๋ต ๋ง์คํฌ์ ์ ์ ๋ณด๋ฅผ ๋ง๋ ๋ค.\"\"\"\n",
" m = np.zeros((SZ, SZ), bool)\n",
" if cls == \"crack\":\n",
" y, x, ang = rng.uniform(12, 52), rng.uniform(12, 52), rng.uniform(0, np.pi)\n",
" for _ in range(int(20 + 40 * sev)):\n",
" ang += rng.normal(0, 0.35); y += np.sin(ang) * 1.2; x += np.cos(ang) * 1.2\n",
" if 0 <= int(y) < SZ and 0 <= int(x) < SZ:\n",
" m[int(y), int(x)] = True\n",
" m = ndi.binary_dilation(m, iterations=1 + int(sev > 0.6))\n",
" elif cls == \"corrosion\":\n",
" for _ in range(int(rng.integers(1, 4))):\n",
" m |= blob(rng, rng.uniform(14, 50), rng.uniform(14, 50), 3 + 9 * sev * rng.uniform(0.6, 1.1))\n",
" elif cls == \"damage\":\n",
" m = blob(rng, rng.uniform(20, 44), rng.uniform(20, 44), 6 + 12 * sev, 0.3)\n",
" elif cls == \"leak\":\n",
" x0, y0 = rng.uniform(16, 48), rng.uniform(8, 24)\n",
" w, ln = 2 + 4 * sev, 12 + 26 * sev\n",
" m = (np.abs(XX - x0 - 0.15 * (YY - y0)) < w * (1 - 0.4 * (YY - y0) / ln)) & (YY > y0) & (YY < y0 + ln)\n",
" return m\n",
"\n",
"\n",
"def make_images(n=600, seed=0, mud=0.0, p_none=0.25, dark=False):\n",
" \"\"\"๊ฐ์ ์ธ๊ด ์ฌ์ง n์ฅ(+์งํ ์ค์ผ mud ๋น์จ)๊ณผ ๊ด๋ฆฌ ๊ธฐ๋กยท์ ๋ต. ๋ฐํ: meta(DataFrame), imgs(n,64,64,3), masks(n,64,64).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" imgs, masks, rows = np.zeros((n, SZ, SZ, 3), np.float32), np.zeros((n, SZ, SZ), bool), []\n",
" for i in range(n):\n",
" cls = \"none\" if rng.random() < p_none else str(rng.choice(CLASSES[1:], p=[0.25, 0.3, 0.25, 0.2]))\n",
" sev = 0.0 if cls == \"none\" else float(rng.beta(1.6, 2.0))\n",
" base = np.array([0.42, 0.46, 0.40]) * (1 + 0.5 * ndi.gaussian_filter(rng.normal(size=(SZ, SZ)), 8)[..., None] * 3)\n",
" img = base + rng.normal(0, 0.012, (SZ, SZ, 3))\n",
" m = draw(rng, cls, sev)\n",
" a = ndi.gaussian_filter(m.astype(float), 0.8)[..., None] # ๊ฒฝ๊ณ๋ฅผ ๋ถ๋๋ฝ๊ฒ ์๋๋ค\n",
" if cls == \"crack\":\n",
" col = img * rng.uniform(0.2, 0.45)\n",
" elif cls == \"corrosion\":\n",
" col = np.array([0.55, 0.30, 0.12]) * rng.uniform(0.75, 1.15) * (0.85 + 0.3 * rng.random((SZ, SZ, 1)))\n",
" elif cls == \"damage\":\n",
" col = np.ones_like(img) * np.array([0.34, 0.34, 0.36]) * (1.0 - 0.45 * sev) * rng.uniform(0.75, 1.0)\n",
" rim = m & ~ndi.binary_erosion(m, iterations=2)\n",
" a = np.maximum(a, 0.0); img = np.where(rim[..., None], 0.7, img)\n",
" elif cls == \"leak\":\n",
" col = np.ones_like(img) * np.array([0.10, 0.09, 0.08]) * rng.uniform(0.8, 2.6)\n",
" else:\n",
" col = img\n",
" img = img * (1 - a) + col * a\n",
" if rng.random() < 0.35: # ๋ฌดํดํ ์ผ๋ฃฉ(๊ฒฐํจ ์๋): ์
์ ๊ฐ์ ๋ฒ์ง\n",
" st = blob(rng, rng.uniform(5, 58), rng.uniform(5, 58), rng.uniform(6, 14), 0.5)\n",
" al = ndi.gaussian_filter(st.astype(float), 2.5)[..., None] * rng.uniform(0.15, 0.4)\n",
" img = img * (1 - al) + np.array([0.50, 0.40, 0.30]) * al\n",
" if rng.random() < 0.3: # ๋ฌดํดํ ๊ธด ์คํฌ๋์น(๋ฐ์ ์ )\n",
" y0, x0, ang = rng.uniform(5, 58), rng.uniform(5, 58), rng.uniform(0, np.pi)\n",
" for t in np.linspace(0, 25, 40):\n",
" yy, xx = int(y0 + t * np.sin(ang)), int(x0 + t * np.cos(ang))\n",
" if 0 <= yy < SZ and 0 <= xx < SZ:\n",
" img[yy, xx] = np.clip(img[yy, xx] * 1.5, 0, 1)\n",
" if mud > 0: # ์งํ ์ค์ผ: ๊ฒฐํจ ์๋ฅผ ๋ฎ์ ์ ์๋ค(์ ๋ต ๋ง์คํฌ๋ ๊ทธ๋๋ก)\n",
" mm = np.zeros((SZ, SZ), bool)\n",
" while mm.mean() < mud:\n",
" mm |= blob(rng, rng.uniform(0, SZ), rng.uniform(0, SZ), rng.uniform(5, 11), 0.5)\n",
" img[mm] = np.array([0.38, 0.30, 0.22]) * (0.85 + 0.3 * rng.random((mm.sum(), 1)))\n",
" gain = rng.uniform(0.3, 0.55) if dark else rng.uniform(0.5, 1.15)\n",
" ang = rng.uniform(0, 2 * np.pi)\n",
" light = gain * (1 + 0.3 * (np.cos(ang) * (XX - 32) + np.sin(ang) * (YY - 32)) / 32)\n",
" imgs[i] = np.clip(img * light[..., None] + rng.normal(0, 0.025, (SZ, SZ, 3)), 0, 1)\n",
" masks[i] = m\n",
" area = float(m.mean())\n",
" depth = {\"none\": 0.0, \"crack\": 0.3 + 3.0 * sev, \"corrosion\": 0.1 + 1.5 * sev, \"damage\": 1.0 + 8.0 * sev, \"leak\": 0.0}[cls] * rng.lognormal(0, 0.1)\n",
" idx = float(np.clip(100 * (1 - np.exp(-(CLS_W[cls] * area * 9 + 0.05 * depth))) + rng.normal(0, 1.5), 0, 100))\n",
" rows.append({\"img_id\": f\"IMG{seed:02d}{i:04d}\", \"cls\": cls, \"sev\": sev, \"area_frac\": area, \"depth_mm\": depth, \"damage_idx\": idx,\n",
" \"grade\": int(grade_of(idx)), \"gain\": gain, \"mud\": mud})\n",
" meta = pd.DataFrame(rows)\n",
" # ์ฅ๋น ๊ด๋ฆฌ ๊ธฐ๋ก(์ ์ ์์ฝ): ์ฌ์ง ํ์ ๊ณผ ๋์กฐํ ๊ฐ\n",
" age = rng.integers(1, 25, n)\n",
" meta[\"day\"] = np.sort(rng.integers(0, 90, n))\n",
" meta[\"equip_type\"] = rng.choice([\"vehicle\", \"generator\", \"comm\", \"trailer\"], n, p=[0.4, 0.2, 0.2, 0.2])\n",
" meta[\"mfg_year\"] = 2026 - age\n",
" meta[\"op_hours\"] = np.clip(rng.normal(180 * age, 400, n) + 8 * meta[\"damage_idx\"], 10, 20000)\n",
" meta[\"env\"] = rng.choice([\"coastal\", \"inland\"], n, p=[0.3, 0.7])\n",
" reg = np.clip(meta[\"grade\"] + rng.choice([-1, 0, 0, 0, 0, 0, 1, 2], n), 1, 3) # ์ฅ๋น ๊ด๋ฆฌ ๊ธฐ๋ก์ ์ง์ ์ ๋น ๋ฑ๊ธ\n",
" meta[\"reg_grade\"] = reg\n",
" meta[\"emergency\"] = rng.random(n) < 0.04 # ๊ธด๊ธ ์ ๊ฒ ์์ฒญ(ํ์ง ๋ถ๋์ด์ด๋ ๋ณด์กด)\n",
" return meta, imgs, masks\n",
"\n",
"# ---- ์ ์ฒ๋ฆฌ(์กฐ๋ช
๋ณด์ ยทํ์คํ)์ ๊ฒฐํจ ํ๋ณด ๋ถํ , ํน์ง ์ถ์ถ (cv2 ๋์ numpyยทscipy ์ฌ์ฉ) ----\n",
"def correct(img):\n",
" \"\"\"์กฐ๋ช
๋ณด์ : ๋ฐ๊ธฐ์ ํ๋ฉด(์กฐ๋ช
๊ธฐ์ธ๊ธฐ)์ ์ ํฉํด ๋๋๋ค. ์ด๋์ด ๊ฒฐํจ ํ์๋ ์ ์ธํ๊ณ ๋ ๋ฒ ๋ฐ๋ณต ์ ํฉํ๋ค. ๋ฐํ: ๋ณด์ ๋ฐ๊ธฐ(์ ์ ๋ฉด ์ฝ 1), ์ ๋น์จ.\"\"\"\n",
" g = img.mean(axis=2)\n",
" A = np.c_[np.ones(SZ * SZ), XX.ravel() / SZ, YY.ravel() / SZ]\n",
" keep = np.ones(SZ * SZ, bool)\n",
" for _ in range(3):\n",
" coef, *_ = np.linalg.lstsq(A[keep], g.ravel()[keep], rcond=None)\n",
" plane = (A @ coef).reshape(SZ, SZ)\n",
" keep = (np.abs(g - plane) < 0.08 * np.maximum(plane, 1e-3)).ravel()\n",
" if keep.sum() < 200:\n",
" keep[:] = True\n",
" c = g / np.maximum(plane, 1e-3)\n",
" chroma = img[..., 0] / (img.sum(axis=2) + 1e-6)\n",
" return c, chroma\n",
"\n",
"\n",
"SEGP = {\"dark\": 0.7, \"rust\": 0.5, \"rim\": 1.45} # ๋ถํ ์๊ณ๊ฐ(์ด๋์ ๋ณด์ ๋ฐ๊ธฐ, ๋ถ์ ๋น์จ, ๋ฐ์ ํ
๋๋ฆฌ): ์ฌ๋ณด์ ๋์\n",
"\n",
"\n",
"def segment(img):\n",
" \"\"\"๊ฒฐํจ ํ๋ณด ๋ง์คํฌ: ์ด๋์ด ์์ญ(๊ท ์ดยท๋์ ยทํ์) + ๋ถ์ ๊ฐ์ ์์ญ(๋ถ์) + ๋ฐ์ ํ
๋๋ฆฌ(ํ์).\"\"\"\n",
" c, chroma = correct(img)\n",
" dark = c < SEGP[\"dark\"]\n",
" rust = (chroma > SEGP[\"rust\"]) & (c > 0.45)\n",
" rim = c > SEGP[\"rim\"]\n",
" m = dark | rust | ndi.binary_dilation(rim, iterations=1) & ndi.binary_dilation(dark, iterations=3)\n",
" m = ndi.binary_opening(m, structure=np.ones((2, 2))) if m.sum() > 12 else m\n",
" return m, c, chroma, dark, rust, rim\n",
"\n",
"\n",
"def extract(img):\n",
" \"\"\"๋ถํ ๊ฒฐ๊ณผ์ ์ยท๋ชจ์์์ 24๊ฐ ํน์ง์ ๋ง๋ ๋ค.\"\"\"\n",
" m, c, chroma, dark, rust, rim = segment(img)\n",
" lab, n = ndi.label(m)\n",
" area = float(m.sum())\n",
" if n:\n",
" sizes = ndi.sum(m, lab, range(1, n + 1))\n",
" k = int(np.argmax(sizes)) + 1\n",
" sl = ndi.find_objects(lab)[k - 1]\n",
" h, w = sl[0].stop - sl[0].start, sl[1].stop - sl[1].start\n",
" big = float(sizes.max())\n",
" elong, fill = max(h, w) / max(min(h, w), 1), big / (h * w)\n",
" else:\n",
" big, elong, fill = 0.0, 1.0, 0.0\n",
" gy, gx = np.gradient(c)\n",
" mag = np.hypot(gx, gy)\n",
" inside = m.sum() > 0\n",
" return {\"area_px\": area, \"area_frac\": area / SZ ** 2, \"n_comp\": float(n), \"big_px\": big, \"elong\": elong, \"fill\": fill,\n",
" \"dark_frac\": float(dark.mean()), \"rust_frac\": float(rust.mean()), \"rim_frac\": float(rim.mean()),\n",
" \"c_mean\": float(c.mean()), \"c_std\": float(c.std()), \"c_min\": float(c.min()), \"edge_mean\": float(mag.mean()), \"edge_p95\": float(np.quantile(mag, 0.95)),\n",
" \"chroma_mean\": float(chroma.mean()), \"chroma_max\": float(chroma.max()),\n",
" \"in_dark\": float(c[m].mean()) if inside else 1.0, \"in_chroma\": float(chroma[m].mean()) if inside else float(chroma.mean()),\n",
" \"ring_contrast\": float(c[ndi.binary_dilation(m, iterations=3) & ~m].mean() - c[m].mean()) if inside and (~m).any() else 0.0,\n",
" \"thin\": float(1 - fill), \"gain\": float(img.mean()), \"sat\": float((img.max(axis=2) - img.min(axis=2)).mean()),\n",
" \"cy\": float(ndi.center_of_mass(m)[0] / SZ) if inside else 0.5, \"cx\": float(ndi.center_of_mass(m)[1] / SZ) if inside else 0.5}\n",
"\n",
"\n",
"def feature_table(imgs):\n",
" return pd.DataFrame([extract(im) for im in imgs])\n",
"\n",
"mtr, itr, _ = make_images(400, seed=1)\n",
"mte, ite, kte = make_images(300, seed=2)\n",
"\n",
"\n",
"def hist_feat(imgs):\n",
" \"\"\"Baseline ํน์ง: ๋ฐ๊ธฐ ํ์คํ ๊ทธ๋จ 16์นธ + ์ฑ๋ ํ๊ท 3 + ๋ฐ๊ธฐ ํ์คํธ์ฐจ 1.\"\"\"\n",
" out = []\n",
" for im in imgs:\n",
" g = im.mean(axis=2)\n",
" h, _ = np.histogram(g / max(g.max(), 1e-3), bins=16, range=(0, 1))\n",
" out.append(np.r_[h / g.size, im.mean(axis=(0, 1)) / max(g.mean(), 1e-3), g.std() / max(g.mean(), 1e-3)])\n",
" return np.array(out)\n",
"\n",
"\n",
"Htr, Hte = hist_feat(itr), hist_feat(ite)\n",
"ytr, yte = (mtr[\"cls\"] != \"none\").values, (mte[\"cls\"] != \"none\").values\n",
"base = make_pipeline(StandardScaler(), LogisticRegression(max_iter=2000, C=0.5)).fit(Htr, ytr)\n",
"pr = base.predict(Hte)\n",
"print(f\"[Baseline ๊ฒฐ๊ณผ] ํ์ต {len(mtr)}์ฅ / ํ๊ฐ {len(mte)}์ฅ, ๊ฒฐํจ ์ฌ์ง {yte.mean():.1%}\")\n",
"print(f\"์ ํ๋ {accuracy_score(yte, pr):.1%}, ์ ๋ฐ๋ {precision_score(yte, pr):.1%}, ์ฌํ์จ {recall_score(yte, pr):.1%}, F1 {f1_score(yte, pr):.3f}\")\n",
"rec = pd.DataFrame({\"cls\": mte[\"cls\"], \"hit\": pr == yte}).query(\"cls != 'none'\").groupby(\"cls\")[\"hit\"].mean().round(2)\n",
"print(\"๊ฒฐํจ ์ ํ๋ณ ์ฌํ์จ:\", \", \".join(f\"{KO_CLS[k]} {v:.0%}\" for k, v in rec.items()))\n",
"small = (mte[\"area_frac\"] > 0) & (mte[\"area_frac\"] < 0.02)\n",
"print(f\"๋ฉด์ 2% ๋ฏธ๋ง ์์ ๊ฒฐํจ {int(small.sum())}์ฅ ์ฌํ์จ {pr[small.values].mean():.0%} (ํฐ ๊ฒฐํจ {pr[(mte['area_frac'] >= 0.02).values].mean():.0%})\")\n",
"dk = mte[\"gain\"] < 0.6\n",
"print(f\"์กฐ๋ช
์ด ์ด๋์ด ์ฌ์ง(gain<0.6) ์ ํ๋ {(pr == yte)[dk.values].mean():.0%} vs ๋ฐ์ ์ฌ์ง {(pr == yte)[~dk.values].mean():.0%}\")\n",
"print(\"-> ์ ์ฒด ํต๊ณ๋ง ๋ณด๋ Baseline์ ์์ ๊ท ์ด์ ๋์น๊ณ ์กฐ๋ช
์ ํ๋ค๋ฆฐ๋ค. ๊ฒฐํจ ์์นยท๋ชจ์์ ๋ณด๋ ๋ถํ ํน์ง์ด ํ์ํ๋ค(๋ฏธ์
4~5์ ๋๊ธฐ).\")\n",
"\n",
"# 1) ์๊ด๋ถ์: ๊ฒฐํจ ์ ๋ฌด์์ ๊ด๊ณ(์ ์ด์ฐ ์๊ด, ์ ์์์ค 0.05) - ์ฌ์ง ํน์ง + ์ฅ๋น ์ ์ ๊ธฐ๋ก\n",
"Fall = pd.concat([feature_table(ite), feature_table(itr[:200])], ignore_index=True)\n",
"mall = pd.concat([mte, mtr[:200]], ignore_index=True)\n",
"Fall[\"op_hours\"], Fall[\"reg_grade\"], Fall[\"mfg_year\"] = mall[\"op_hours\"], mall[\"reg_grade\"], mall[\"mfg_year\"]\n",
"Fall[\"coastal\"] = (mall[\"env\"] == \"coastal\").astype(int)\n",
"yall = (mall[\"cls\"] != \"none\").astype(float)\n",
"rows = []\n",
"for f in [\"c_std\", \"c_min\", \"edge_p95\", \"dark_frac\", \"rust_frac\", \"sat\", \"op_hours\", \"reg_grade\", \"mfg_year\", \"coastal\"]:\n",
" r, p = stats.pearsonr(Fall[f].astype(float), yall)\n",
" rows.append({\"ํผ์ฒ\": f, \"์๊ด๊ณ์\": round(r, 3), \"p-value\": f\"{p:.1e}\", \"์ ์\": p < 0.05})\n",
"corr = pd.DataFrame(rows).sort_values(\"์๊ด๊ณ์\", key=abs, ascending=False)\n",
"print(\"\\n[๊ฒฐํจ ์ ๋ฌด์์ ์๊ด๋ถ์]\")\n",
"print(corr.to_string(index=False))\n",
"n_sig = int(corr[\"์ ์\"].sum())\n",
"print(f\"์ ์๋ฏธํ ํผ์ฒ {n_sig}๊ฐ (๋ชฉํ 5๊ฐ ์ด์: {'๋ฌ์ฑ' if n_sig >= 5 else '๋ฏธ๋ฌ'})\")\n",
"\n",
"# 2) ์ ์ ๊ธฐ๋ก๊ณผ์ ๋ถ์ผ์น: ์ฌ์ง ํ์ ๋ฑ๊ธ vs ๊ด๋ฆฌ ๊ธฐ๋ก ๋ฑ๊ธ\n",
"ct = pd.crosstab(mall[\"grade\"], mall[\"reg_grade\"]).rename(index=GRADE_NAME, columns=GRADE_NAME)\n",
"gap = (mall[\"grade\"] - mall[\"reg_grade\"]).abs() >= 2\n",
"print(f\"\\n[์ ์ ๋์กฐ] ์ฌ์ง ๋ฑ๊ธ๊ณผ ๊ด๋ฆฌ ๊ธฐ๋ก ๋ฑ๊ธ์ด 2๋จ๊ณ ์ด์ ๋ค๋ฅธ ์ฅ๋น {int(gap.sum())}๋({gap.mean():.1%}) -> ์ ์ ์ด์ ์ ํธ ํ๋ณด\")\n",
"print(ct.to_string())\n",
"\n",
"# 3) ํ๋ ํ (ABC): ์ ๋น ๊ณต์(๊ฐ์)๊ฐ ํฐ (์ฅ๋น ๊ตฌ๋ถ, ๊ฒฐํจ ์ ํ) ์กฐํฉ\n",
"HOURS = {1: 2, 2: 8, 3: 30}\n",
"mall[\"hours\"] = mall[\"grade\"].map(HOURS)\n",
"g = mall[mall[\"cls\"] != \"none\"].groupby([\"equip_type\", \"cls\"])[\"hours\"].sum().sort_values(ascending=False)\n",
"cum = g.cumsum() / g.sum()\n",
"abc = pd.cut(cum, [0, 0.8, 0.95, 1.0], labels=[\"A\", \"B\", \"C\"])\n",
"print(\"\\n[ํ๋ ํ A๋ฑ๊ธ(๋์ ์ ๋น ๊ณต์ 80%)]\", \", \".join(f\"{e}/{KO_CLS[c]}\" for (e, c), v in abc.items() if v == \"A\"))\n",
"print(f\"A๋ฑ๊ธ์ ์ ์ฒด ์กฐํฉ {len(g)}๊ฐ ์ค {int((abc == 'A').sum())}๊ฐ({(abc == 'A').mean():.0%})\")\n",
"\n",
"# 4) EDA ์ฐจํธ 8์ข
\n",
"fig, ax = plt.subplots(2, 4, figsize=(18, 8))\n",
"a = ax.ravel()\n",
"a[0].imshow(np.concatenate([ite[int(np.where(mte['cls'] == c)[0][0])] for c in CLASSES], axis=1)); a[0].set_title(\"1. Samples: none/crack/corrosion/damage/leak\"); a[0].axis(\"off\")\n",
"mall[\"cls\"].value_counts().plot.bar(ax=a[1]); a[1].set_title(\"2. Photos by defect class\")\n",
"a[2].hist(mall.loc[yall == 0, \"gain\"], bins=20, alpha=0.6, label=\"none\"); a[2].hist(mall.loc[yall == 1, \"gain\"], bins=20, alpha=0.6, label=\"defect\"); a[2].legend(); a[2].set_title(\"3. Lighting gain\")\n",
"mall.boxplot(column=\"damage_idx\", by=\"cls\", ax=a[3], rot=30); a[3].set_title(\"4. Damage index by class\")\n",
"a[4].imshow(ct.values, cmap=\"Blues\"); a[4].set_xticks(range(3), [1, 2, 3]); a[4].set_yticks(range(3), [1, 2, 3]); a[4].set_xlabel(\"registered grade\"); a[4].set_ylabel(\"photo grade\"); a[4].set_title(\"5. Photo vs registered grade\")\n",
"a[5].scatter(mall[\"op_hours\"], mall[\"damage_idx\"], s=6, c=mall[\"env\"].map({\"coastal\": \"tab:red\", \"inland\": \"tab:blue\"})); a[5].set_title(\"6. Op hours vs damage (red = coastal)\")\n",
"a[6].barh(corr[\"ํผ์ฒ\"], corr[\"์๊ด๊ณ์\"]); a[6].set_title(\"7. Correlation with defect\")\n",
"a[7].plot(range(1, len(cum) + 1), cum.values, \"o-\"); a[7].axhline(0.8, ls=\"--\", c=\"gray\"); a[7].set_title(\"8. Pareto of repair hours\")\n",
"plt.suptitle(\"QLT03 EDA - baseline defect screening (synthetic photos)\"); plt.tight_layout(); plt.show()\n",
"print(\"\\nEDA ์ฐจํธ 8์ข
์์ฑ (๋ชฉํ 8์ข
์ด์: ๋ฌ์ฑ)\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
03 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[Baseline ๊ฒฐ๊ณผ] ํ์ต 400์ฅ / ํ๊ฐ 300์ฅ, ๊ฒฐํจ ์ฌ์ง 76.7%\n",
"์ ํ๋ 86.0%, ์ ๋ฐ๋ 89.8%, ์ฌํ์จ 92.2%, F1 0.910\n",
"๊ฒฐํจ ์ ํ๋ณ ์ฌํ์จ: ๋ถ์ 96%, ๊ท ์ด 75%, ํ์ 96%, ๋์ 98%\n",
"๋ฉด์ 2% ๋ฏธ๋ง ์์ ๊ฒฐํจ 31์ฅ ์ฌํ์จ 74% (ํฐ ๊ฒฐํจ 95%)\n",
"์กฐ๋ช
์ด ์ด๋์ด ์ฌ์ง(gain<0.6) ์ ํ๋ 82% vs ๋ฐ์ ์ฌ์ง 87%\n",
"-> ์ ์ฒด ํต๊ณ๋ง ๋ณด๋ Baseline์ ์์ ๊ท ์ด์ ๋์น๊ณ ์กฐ๋ช
์ ํ๋ค๋ฆฐ๋ค. ๊ฒฐํจ ์์นยท๋ชจ์์ ๋ณด๋ ๋ถํ ํน์ง์ด ํ์ํ๋ค(๋ฏธ์
4~5์ ๋๊ธฐ).\n",
"\n",
"[๊ฒฐํจ ์ ๋ฌด์์ ์๊ด๋ถ์]\n",
" ํผ์ฒ ์๊ด๊ณ์ p-value ์ ์\n",
" c_min -0.711 2.7e-78 True\n",
" c_std 0.496 1.8e-32 True\n",
"dark_frac 0.457 3.5e-27 True\n",
" edge_p95 0.373 5.4e-18 True\n",
"reg_grade 0.272 6.2e-10 True\n",
"rust_frac 0.242 4.5e-08 True\n",
" sat 0.186 3.0e-05 True\n",
" op_hours 0.066 1.4e-01 False\n",
" mfg_year 0.045 3.1e-01 False\n",
" coastal 0.004 9.2e-01 False\n",
"์ ์๋ฏธํ ํผ์ฒ 7๊ฐ (๋ชฉํ 5๊ฐ ์ด์: ๋ฌ์ฑ)\n",
"\n",
"[์ ์ ๋์กฐ] ์ฌ์ง ๋ฑ๊ธ๊ณผ ๊ด๋ฆฌ ๊ธฐ๋ก ๋ฑ๊ธ์ด 2๋จ๊ณ ์ด์ ๋ค๋ฅธ ์ฅ๋น 47๋(9.4%) -> ์ ์ ์ด์ ์ ํธ ํ๋ณด\n",
"reg_grade ํ์ฅ์ ๋น ์ผ์ ์ ๋น ์ฐฝ์ ๋น\n",
"grade \n",
"ํ์ฅ์ ๋น 219 50 47\n",
"์ผ์ ์ ๋น 8 58 28\n",
"์ฐฝ์ ๋น 0 15 75\n",
"\n",
"[ํ๋ ํ A๋ฑ๊ธ(๋์ ์ ๋น ๊ณต์ 80%)] vehicle/ํ์, comm/ํ์, vehicle/๋ถ์, trailer/ํ์, generator/ํ์, generator/๋ถ์\n",
"A๋ฑ๊ธ์ ์ ์ฒด ์กฐํฉ 16๊ฐ ์ค 6๊ฐ(38%)\n",
"\n",
"EDA ์ฐจํธ 8์ข
์์ฑ (๋ชฉํ 8์ข
์ด์: ๋ฌ์ฑ)\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
04. ์ธ๊ทธ๋ฉํ
์ด์
(Mask R-CNN) ๊ธฐ๋ฐ ๊ฒฐํจ ๋ฉด์ /๊น์ด ์ ๋ฐ ์ธก์ ๋ฐ ์์๋ ์๋ ํ์ ๋ชจ๋ธ ๊ณ ๋ํ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ธ๊ทธ๋ฉํ
์ด์
๊ธฐ๋ฐ ๊ฒฐํจ ๋ฉด์ /๊น์ด ์ ๋ฐ ์ธก์ ๋ฐ ์์๋ ์๋ ํ์ ๋ชจ๋ธ์ ๊ณ ๋ํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๋ถํ ๊ธฐ๋ฐ์ผ๋ก ๊ฒฐํจ ๋ฉด์ ๊ณผ ๊น์ด๋ฅผ ์ธก์ ํ๊ณ ์์๋๋ฅผ ์๋ ํ์ ํ๋ ๋ชจ๋ธ์ ๋ง๋ค์ด ๊ธฐ์ค ์ค์ฐจ๋ฅผ ์ธก์ ํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์กฐ๋ช
๋ณด์ ํ ์๊ณ๊ฐ ๋ถํ (Mask R-CNN ๋์ฒด)๋ก ๊ฒฐํจ ๋ง์คํฌ๋ฅผ ๋ง๋ค๊ณ ์ ๋ต ๋ง์คํฌ์์ IoUยทDice๋ก ์ ๋ฐ๋ ํ๊ฐํ๊ณ ์ฑ๋ฅ์ ๋น๊ตํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/features/qlt03_feature_engineering.py ๋ฐ ๊ธฐ์ค ๋ชจ๋ธ` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๊ตฐ์ ์ค๋ฌด์์ ๋ฉ๋ ๊ฐ๋ฅํ ํผ์ฒ ์ค๊ณ์ ๋
ผ๋ฆฌ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ํ์ ํผ์ฒ 10๊ฐ ์ด์ ์์ฑ, ๊ธฐ์ค ๋ชจ๋ธ MAPE 25% ์ดํ ๋ฌ์ฑ\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ํผ์ฒ ํ์ดํ๋ผ์ธ ๋ฐ ๋ฒ ์ด์ค๋ผ์ธ ๋ชจ๋ธ ์ฑ๋ฅ ์งํ ๋์ถ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ๊ต์ฐจ ๊ฒ์ฆ(Time-series Cross Validation)์ ํตํ ๊ณผ์ ํฉ ๋ฐฉ์ง\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"๋ถํ (thresholding ๊ธฐ๋ฐ ๊ฒฐํจ ํ๋ณด ๋ถํ , Mask R-CNN ๋์ฒด) ๊ฒฐ๊ณผ์์ ๊ฒฐํจ ๋ฉด์ ์ ์ธก์ ยท๋ณด์ ํ๊ณ ๊น์ด์ ์์๋ ์ ์๋ฅผ ํ์ตํด ์์๋๋ฅผ ์๋ ํ์ ํ๋ฉฐ, ๋ถํ IoUยท๋ฉด์ /๊น์ด/์์๋ ์ค์ฐจ(MAPE)์ ๋ณ์๊ตฐ ์ ๊ฑฐ ์คํ์ผ๋ก ํ์ ํผ์ฒ ์ค๊ณ์ ํ๋น์ฑ์ ๊ฒ์ฆํ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.ensemble import RandomForestClassifier, GradientBoostingRegressor\n",
"from sklearn.linear_model import LinearRegression\n",
"from sklearn.model_selection import cross_val_predict\n",
"from sklearn.metrics import r2_score\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ฅ๋น ์ธ๊ด ์ฌ์ง ์์ฑ๊ธฐ (์ค์ ์ฅ๋นยท๋ถ๋ยท์์น ์ฌ์ง์ด ์๋๋ผ ๋์๋ก ๊ทธ๋ฆฐ 64x64 ํฉ์ฑ ์ด๋ฏธ์ง)\n",
"# ๋์ฅ๋ ๊ธ์ ํ๋ฉด์ ๊ท ์ด(crack)ยท๋ถ์(corrosion)ยทํ์(damage)ยท๋์ (leak)๋ฅผ ๊ทธ๋ฆฌ๊ณ , ์กฐ๋ช
ํธ์ฐจ์ ์งํ(mud) ์ค์ผ์ ์๋๋ค.\n",
"# ์ด๋ฏธ์ง์ ํจ๊ป ์ฅ๋น ๊ด๋ฆฌ ๊ธฐ๋ก(์ ์ ์์ฝ)๊ณผ ์ ๋ต ๊ฒฐํจ ๋ง์คํฌยท์์๋ยท์ ๋น ๋ฑ๊ธ์ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"from scipy import ndimage as ndi\n",
"\n",
"SZ = 64\n",
"CLASSES = [\"none\", \"crack\", \"corrosion\", \"damage\", \"leak\"]\n",
"KO_CLS = {\"none\": \"๊ฒฐํจ ์์\", \"crack\": \"๊ท ์ด\", \"corrosion\": \"๋ถ์\", \"damage\": \"ํ์\", \"leak\": \"๋์ \"}\n",
"CLS_W = {\"none\": 0.0, \"crack\": 1.4, \"corrosion\": 1.0, \"damage\": 1.6, \"leak\": 0.6} # ์์๋ ๊ณ์ฐ์ฉ ๊ฒฐํจ ์ ํ ๊ฐ์ค์น\n",
"GRADE_NAME = {1: \"ํ์ฅ์ ๋น\", 2: \"์ผ์ ์ ๋น\", 3: \"์ฐฝ์ ๋น\"}\n",
"YY, XX = np.mgrid[0:SZ, 0:SZ]\n",
"\n",
"\n",
"def grade_of(idx):\n",
" # [TODO : ํ์ ์ค์ต 4] ์ธ๊ทธ๋ฉํ
์ด์
(Mask R-CNN) ๊ธฐ๋ฐ ๊ฒฐํจ ๋ฉด์ /๊น์ด ์ ๋ฐ ์ธก์ ๋ฐ ์์๋ ์๋ ํ์ ๋ชจ๋ธ ๊ณ ๋ํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def blob(rng, cy, cx, r, rough=0.35):\n",
" \"\"\"๋ถ๊ท์นํ ์ํ ์์ญ ๋ง์คํฌ.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 4] ์ธ๊ทธ๋ฉํ
์ด์
(Mask R-CNN) ๊ธฐ๋ฐ ๊ฒฐํจ ๋ฉด์ /๊น์ด ์ ๋ฐ ์ธก์ ๋ฐ ์์๋ ์๋ ํ์ ๋ชจ๋ธ ๊ณ ๋ํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def draw(rng, cls, sev):\n",
" \"\"\"๊ฒฐํจ ์ ํยท์ฌ๊ฐ๋ sev(0~1)์ ๋ง๋ ์ ๋ต ๋ง์คํฌ์ ์ ์ ๋ณด๋ฅผ ๋ง๋ ๋ค.\"\"\"\n",
" m = np.zeros((SZ, SZ), bool)\n",
" if cls == \"crack\":\n",
" y, x, ang = rng.uniform(12, 52), rng.uniform(12, 52), rng.uniform(0, np.pi)\n",
" for _ in range(int(20 + 40 * sev)):\n",
" ang += rng.normal(0, 0.35); y += np.sin(ang) * 1.2; x += np.cos(ang) * 1.2\n",
" if 0 <= int(y) < SZ and 0 <= int(x) < SZ:\n",
" m[int(y), int(x)] = True\n",
" m = ndi.binary_dilation(m, iterations=1 + int(sev > 0.6))\n",
" elif cls == \"corrosion\":\n",
" for _ in range(int(rng.integers(1, 4))):\n",
" m |= blob(rng, rng.uniform(14, 50), rng.uniform(14, 50), 3 + 9 * sev * rng.uniform(0.6, 1.1))\n",
" elif cls == \"damage\":\n",
" m = blob(rng, rng.uniform(20, 44), rng.uniform(20, 44), 6 + 12 * sev, 0.3)\n",
" elif cls == \"leak\":\n",
" x0, y0 = rng.uniform(16, 48), rng.uniform(8, 24)\n",
" w, ln = 2 + 4 * sev, 12 + 26 * sev\n",
" m = (np.abs(XX - x0 - 0.15 * (YY - y0)) < w * (1 - 0.4 * (YY - y0) / ln)) & (YY > y0) & (YY < y0 + ln)\n",
" return m\n",
"\n",
"\n",
"def make_images(n=600, seed=0, mud=0.0, p_none=0.25, dark=False):\n",
" \"\"\"๊ฐ์ ์ธ๊ด ์ฌ์ง n์ฅ(+์งํ ์ค์ผ mud ๋น์จ)๊ณผ ๊ด๋ฆฌ ๊ธฐ๋กยท์ ๋ต. ๋ฐํ: meta(DataFrame), imgs(n,64,64,3), masks(n,64,64).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" imgs, masks, rows = np.zeros((n, SZ, SZ, 3), np.float32), np.zeros((n, SZ, SZ), bool), []\n",
" for i in range(n):\n",
" cls = \"none\" if rng.random() < p_none else str(rng.choice(CLASSES[1:], p=[0.25, 0.3, 0.25, 0.2]))\n",
" sev = 0.0 if cls == \"none\" else float(rng.beta(1.6, 2.0))\n",
" base = np.array([0.42, 0.46, 0.40]) * (1 + 0.5 * ndi.gaussian_filter(rng.normal(size=(SZ, SZ)), 8)[..., None] * 3)\n",
" img = base + rng.normal(0, 0.012, (SZ, SZ, 3))\n",
" m = draw(rng, cls, sev)\n",
" a = ndi.gaussian_filter(m.astype(float), 0.8)[..., None] # ๊ฒฝ๊ณ๋ฅผ ๋ถ๋๋ฝ๊ฒ ์๋๋ค\n",
" if cls == \"crack\":\n",
" col = img * rng.uniform(0.2, 0.45)\n",
" elif cls == \"corrosion\":\n",
" col = np.array([0.55, 0.30, 0.12]) * rng.uniform(0.75, 1.15) * (0.85 + 0.3 * rng.random((SZ, SZ, 1)))\n",
" elif cls == \"damage\":\n",
" col = np.ones_like(img) * np.array([0.34, 0.34, 0.36]) * (1.0 - 0.45 * sev) * rng.uniform(0.75, 1.0)\n",
" rim = m & ~ndi.binary_erosion(m, iterations=2)\n",
" a = np.maximum(a, 0.0); img = np.where(rim[..., None], 0.7, img)\n",
" elif cls == \"leak\":\n",
" col = np.ones_like(img) * np.array([0.10, 0.09, 0.08]) * rng.uniform(0.8, 2.6)\n",
" else:\n",
" col = img\n",
" img = img * (1 - a) + col * a\n",
" if rng.random() < 0.35: # ๋ฌดํดํ ์ผ๋ฃฉ(๊ฒฐํจ ์๋): ์
์ ๊ฐ์ ๋ฒ์ง\n",
" st = blob(rng, rng.uniform(5, 58), rng.uniform(5, 58), rng.uniform(6, 14), 0.5)\n",
" al = ndi.gaussian_filter(st.astype(float), 2.5)[..., None] * rng.uniform(0.15, 0.4)\n",
" img = img * (1 - al) + np.array([0.50, 0.40, 0.30]) * al\n",
" if rng.random() < 0.3: # ๋ฌดํดํ ๊ธด ์คํฌ๋์น(๋ฐ์ ์ )\n",
" y0, x0, ang = rng.uniform(5, 58), rng.uniform(5, 58), rng.uniform(0, np.pi)\n",
" for t in np.linspace(0, 25, 40):\n",
" yy, xx = int(y0 + t * np.sin(ang)), int(x0 + t * np.cos(ang))\n",
" if 0 <= yy < SZ and 0 <= xx < SZ:\n",
" img[yy, xx] = np.clip(img[yy, xx] * 1.5, 0, 1)\n",
" if mud > 0: # ์งํ ์ค์ผ: ๊ฒฐํจ ์๋ฅผ ๋ฎ์ ์ ์๋ค(์ ๋ต ๋ง์คํฌ๋ ๊ทธ๋๋ก)\n",
" mm = np.zeros((SZ, SZ), bool)\n",
" while mm.mean() < mud:\n",
" mm |= blob(rng, rng.uniform(0, SZ), rng.uniform(0, SZ), rng.uniform(5, 11), 0.5)\n",
" img[mm] = np.array([0.38, 0.30, 0.22]) * (0.85 + 0.3 * rng.random((mm.sum(), 1)))\n",
" gain = rng.uniform(0.3, 0.55) if dark else rng.uniform(0.5, 1.15)\n",
" ang = rng.uniform(0, 2 * np.pi)\n",
" light = gain * (1 + 0.3 * (np.cos(ang) * (XX - 32) + np.sin(ang) * (YY - 32)) / 32)\n",
" imgs[i] = np.clip(img * light[..., None] + rng.normal(0, 0.025, (SZ, SZ, 3)), 0, 1)\n",
" masks[i] = m\n",
" area = float(m.mean())\n",
" depth = {\"none\": 0.0, \"crack\": 0.3 + 3.0 * sev, \"corrosion\": 0.1 + 1.5 * sev, \"damage\": 1.0 + 8.0 * sev, \"leak\": 0.0}[cls] * rng.lognormal(0, 0.1)\n",
" idx = float(np.clip(100 * (1 - np.exp(-(CLS_W[cls] * area * 9 + 0.05 * depth))) + rng.normal(0, 1.5), 0, 100))\n",
" rows.append({\"img_id\": f\"IMG{seed:02d}{i:04d}\", \"cls\": cls, \"sev\": sev, \"area_frac\": area, \"depth_mm\": depth, \"damage_idx\": idx,\n",
" \"grade\": int(grade_of(idx)), \"gain\": gain, \"mud\": mud})\n",
" meta = pd.DataFrame(rows)\n",
" # ์ฅ๋น ๊ด๋ฆฌ ๊ธฐ๋ก(์ ์ ์์ฝ): ์ฌ์ง ํ์ ๊ณผ ๋์กฐํ ๊ฐ\n",
" age = rng.integers(1, 25, n)\n",
" meta[\"day\"] = np.sort(rng.integers(0, 90, n))\n",
" meta[\"equip_type\"] = rng.choice([\"vehicle\", \"generator\", \"comm\", \"trailer\"], n, p=[0.4, 0.2, 0.2, 0.2])\n",
" meta[\"mfg_year\"] = 2026 - age\n",
" meta[\"op_hours\"] = np.clip(rng.normal(180 * age, 400, n) + 8 * meta[\"damage_idx\"], 10, 20000)\n",
" meta[\"env\"] = rng.choice([\"coastal\", \"inland\"], n, p=[0.3, 0.7])\n",
" reg = np.clip(meta[\"grade\"] + rng.choice([-1, 0, 0, 0, 0, 0, 1, 2], n), 1, 3) # ์ฅ๋น ๊ด๋ฆฌ ๊ธฐ๋ก์ ์ง์ ์ ๋น ๋ฑ๊ธ\n",
" meta[\"reg_grade\"] = reg\n",
" meta[\"emergency\"] = rng.random(n) < 0.04 # ๊ธด๊ธ ์ ๊ฒ ์์ฒญ(ํ์ง ๋ถ๋์ด์ด๋ ๋ณด์กด)\n",
" return meta, imgs, masks\n",
"# ---- ์ ์ฒ๋ฆฌ(์กฐ๋ช
๋ณด์ ยทํ์คํ)์ ๊ฒฐํจ ํ๋ณด ๋ถํ , ํน์ง ์ถ์ถ (cv2 ๋์ numpyยทscipy ์ฌ์ฉ) ----\n",
"def correct(img):\n",
" \"\"\"์กฐ๋ช
๋ณด์ : ๋ฐ๊ธฐ์ ํ๋ฉด(์กฐ๋ช
๊ธฐ์ธ๊ธฐ)์ ์ ํฉํด ๋๋๋ค. ์ด๋์ด ๊ฒฐํจ ํ์๋ ์ ์ธํ๊ณ ๋ ๋ฒ ๋ฐ๋ณต ์ ํฉํ๋ค. ๋ฐํ: ๋ณด์ ๋ฐ๊ธฐ(์ ์ ๋ฉด ์ฝ 1), ์ ๋น์จ.\"\"\"\n",
" g = img.mean(axis=2)\n",
" A = np.c_[np.ones(SZ * SZ), XX.ravel() / SZ, YY.ravel() / SZ]\n",
" keep = np.ones(SZ * SZ, bool)\n",
" for _ in range(3):\n",
" coef, *_ = np.linalg.lstsq(A[keep], g.ravel()[keep], rcond=None)\n",
" plane = (A @ coef).reshape(SZ, SZ)\n",
" keep = (np.abs(g - plane) < 0.08 * np.maximum(plane, 1e-3)).ravel()\n",
" if keep.sum() < 200:\n",
" keep[:] = True\n",
" c = g / np.maximum(plane, 1e-3)\n",
" chroma = img[..., 0] / (img.sum(axis=2) + 1e-6)\n",
" return c, chroma\n",
"\n",
"\n",
"SEGP = {\"dark\": 0.7, \"rust\": 0.5, \"rim\": 1.45} # ๋ถํ ์๊ณ๊ฐ(์ด๋์ ๋ณด์ ๋ฐ๊ธฐ, ๋ถ์ ๋น์จ, ๋ฐ์ ํ
๋๋ฆฌ): ์ฌ๋ณด์ ๋์\n",
"\n",
"\n",
"def segment(img):\n",
" \"\"\"๊ฒฐํจ ํ๋ณด ๋ง์คํฌ: ์ด๋์ด ์์ญ(๊ท ์ดยท๋์ ยทํ์) + ๋ถ์ ๊ฐ์ ์์ญ(๋ถ์) + ๋ฐ์ ํ
๋๋ฆฌ(ํ์).\"\"\"\n",
" c, chroma = correct(img)\n",
" dark = c < SEGP[\"dark\"]\n",
" rust = (chroma > SEGP[\"rust\"]) & (c > 0.45)\n",
" rim = c > SEGP[\"rim\"]\n",
" m = dark | rust | ndi.binary_dilation(rim, iterations=1) & ndi.binary_dilation(dark, iterations=3)\n",
" m = ndi.binary_opening(m, structure=np.ones((2, 2))) if m.sum() > 12 else m\n",
" return m, c, chroma, dark, rust, rim\n",
"\n",
"\n",
"def extract(img):\n",
" \"\"\"๋ถํ ๊ฒฐ๊ณผ์ ์ยท๋ชจ์์์ 24๊ฐ ํน์ง์ ๋ง๋ ๋ค.\"\"\"\n",
" m, c, chroma, dark, rust, rim = segment(img)\n",
" lab, n = ndi.label(m)\n",
" area = float(m.sum())\n",
" if n:\n",
" sizes = ndi.sum(m, lab, range(1, n + 1))\n",
" k = int(np.argmax(sizes)) + 1\n",
" sl = ndi.find_objects(lab)[k - 1]\n",
" h, w = sl[0].stop - sl[0].start, sl[1].stop - sl[1].start\n",
" big = float(sizes.max())\n",
" elong, fill = max(h, w) / max(min(h, w), 1), big / (h * w)\n",
" else:\n",
" big, elong, fill = 0.0, 1.0, 0.0\n",
" gy, gx = np.gradient(c)\n",
" mag = np.hypot(gx, gy)\n",
" inside = m.sum() > 0\n",
" return {\"area_px\": area, \"area_frac\": area / SZ ** 2, \"n_comp\": float(n), \"big_px\": big, \"elong\": elong, \"fill\": fill,\n",
" \"dark_frac\": float(dark.mean()), \"rust_frac\": float(rust.mean()), \"rim_frac\": float(rim.mean()),\n",
" \"c_mean\": float(c.mean()), \"c_std\": float(c.std()), \"c_min\": float(c.min()), \"edge_mean\": float(mag.mean()), \"edge_p95\": float(np.quantile(mag, 0.95)),\n",
" \"chroma_mean\": float(chroma.mean()), \"chroma_max\": float(chroma.max()),\n",
" \"in_dark\": float(c[m].mean()) if inside else 1.0, \"in_chroma\": float(chroma[m].mean()) if inside else float(chroma.mean()),\n",
" \"ring_contrast\": float(c[ndi.binary_dilation(m, iterations=3) & ~m].mean() - c[m].mean()) if inside and (~m).any() else 0.0,\n",
" \"thin\": float(1 - fill), \"gain\": float(img.mean()), \"sat\": float((img.max(axis=2) - img.min(axis=2)).mean()),\n",
" \"cy\": float(ndi.center_of_mass(m)[0] / SZ) if inside else 0.5, \"cx\": float(ndi.center_of_mass(m)[1] / SZ) if inside else 0.5}\n",
"\n",
"\n",
"def feature_table(imgs):\n",
" return pd.DataFrame([extract(im) for im in imgs])\n",
"# ---- ํ์ ํ์ดํ๋ผ์ธ: ๊ฒฐํจ ์ ํ ๋ถ๋ฅ + ์์๋ ํ๊ท(๋ถ๋ฅ ํ๋ฅ ํฌํจ) -> ์ ๋น ๋ฑ๊ธ ----\n",
"def fit_pipeline(meta, F):\n",
" \"\"\"๊ฒฐํจ ์ ํ ๋ถ๋ฅ๊ธฐ์ ์์๋ ํ๊ท๊ธฐ๋ฅผ ํ์ตํ๋ค. ํ๊ท๊ธฐ ์
๋ ฅ์ ๋ถ๋ฅ ํ๋ฅ ์ ๊ต์ฐจ ์์ธก์ผ๋ก ๋ง๋ค์ด ๋์ค์ ๋ง๋๋ค.\"\"\"\n",
" clf = RandomForestClassifier(n_estimators=150, random_state=0, n_jobs=1)\n",
" P = cross_val_predict(clf, F, meta[\"cls\"], cv=4, method=\"predict_proba\")\n",
" clf.fit(F, meta[\"cls\"])\n",
" reg = GradientBoostingRegressor(n_estimators=200, max_depth=3, learning_rate=0.06, random_state=0).fit(np.c_[F.values, P], meta[\"damage_idx\"])\n",
" return {\"clf\": clf, \"reg\": reg, \"cols\": list(F.columns)}\n",
"\n",
"\n",
"def predict_pipeline(pl, F):\n",
" X = F[pl[\"cols\"]]\n",
" P = pl[\"clf\"].predict_proba(X)\n",
" idx = np.clip(pl[\"reg\"].predict(np.c_[X.values, P]), 0, 100)\n",
" return pl[\"clf\"].classes_[P.argmax(axis=1)], P, idx, P.max(axis=1)\n",
"\n",
"mtr, itr, ktr = make_images(600, seed=1)\n",
"mte, ite, kte = make_images(300, seed=2)\n",
"Ftr, Fte = feature_table(itr), feature_table(ite)\n",
"print(f\"[1] ํ์ ํผ์ฒ {Ftr.shape[1]}๊ฐ(๋ถํ ๋ฉด์ ยท์ฑ๋ถ ์ยท๋ชจ์, ๋ช
์, ์, ๊ฐ์ฅ์๋ฆฌ, ์์น) / ํ์ต {len(mtr)}์ฅ, ํ๊ฐ {len(mte)}์ฅ (1ํ์ = 1.5mm ๊ฐ์ )\")\n",
"\n",
"# ---------------- 1) ๋ถํ ์ ๋ฐ๋: ์ ๋ต ๋ง์คํฌ์์ IoU ----------------\n",
"seg = [segment(im)[0] for im in ite]\n",
"rows = []\n",
"for c in CLASSES[1:]:\n",
" ix = np.where(mte[\"cls\"].values == c)[0]\n",
" iou = [(seg[i] & kte[i]).sum() / max((seg[i] | kte[i]).sum(), 1) for i in ix]\n",
" dice = [2 * (seg[i] & kte[i]).sum() / max(seg[i].sum() + kte[i].sum(), 1) for i in ix]\n",
" hit = [seg[i].sum() > 0 and (seg[i] & kte[i]).sum() > 0 for i in ix]\n",
" rows.append((KO_CLS[c], len(ix), np.mean(iou), np.mean(dice), np.mean(hit)))\n",
"sg = pd.DataFrame(rows, columns=[\"๊ฒฐํจ\", \"์ฅ์\", \"ํ๊ท IoU\", \"ํ๊ท Dice\", \"๊ฒ์ถ๋ฅ \"])\n",
"print(\"[2] ๋ถํ ์ฑ๋ฅ(ํ๊ฐ 300์ฅ)\\n\" + sg.round(2).to_string(index=False))\n",
"ok0 = np.mean([seg[i].sum() == 0 or (seg[i].sum() < 12) for i in np.where(mte[\"cls\"].values == \"none\")[0]])\n",
"print(f\" ๊ฒฐํจ ์๋ ์ฌ์ง {int((mte['cls'] == 'none').sum())}์ฅ ์ค ๋ถํ ๊ฒฐ๊ณผ๊ฐ ๊ฑฐ์ ๋น์ด ์๋(12ํ์ ๋ฏธ๋ง) ์ฌ์ง {ok0:.0%} (๋ฌดํดํ ์ผ๋ฃฉยท์คํฌ๋์น ์คํ ์ต์ )\")\n",
"\n",
"# ---------------- 2) ๋ฉด์ ์ธก์ ๊ณผ ์ ํ๋ณ ๋ณด์ ----------------\n",
"pl = fit_pipeline(mtr, Ftr)\n",
"cls_te, P_te, idx_te, conf_te = predict_pipeline(pl, Fte)\n",
"a_true, a_raw = mte[\"area_frac\"].values, Fte[\"area_frac\"].values\n",
"cal = {c: LinearRegression().fit(Ftr.loc[mtr[\"cls\"].values == c, [\"area_frac\"]], mtr.loc[mtr[\"cls\"].values == c, \"area_frac\"]) for c in CLASSES[1:]}\n",
"a_cal = np.array([cal[c].predict(pd.DataFrame({\"area_frac\": [a]}))[0] if c != \"none\" else 0.0 for c, a in zip(cls_te, a_raw)])\n",
"m = a_true > 0\n",
"mp = lambda y, p: float(np.mean(np.abs(y - p) / y) * 100)\n",
"print(f\"\\n[3] ๋ฉด์ ์ธก์ (๊ฒฐํจ {int(m.sum())}์ฅ): ๋ถํ ์์ธก์ MAPE {mp(a_true[m], a_raw[m]):.1f}% -> ์ ํ๋ณ ์ ํ ๋ณด์ ํ {mp(a_true[m], a_cal[m]):.1f}% (๋ถ๋ฅ ์ ํ๋ {(cls_te == mte['cls'].values).mean():.1%})\")\n",
"print(f\" ๋ฉด์ ํ์ฐ ์: ํ๊ท ๊ฒฐํจ ๋ฉด์ {a_true[m].mean() * SZ ** 2 * 1.5 ** 2:.0f}mm2(์ ๋ต) / {a_cal[m].mean() * SZ ** 2 * 1.5 ** 2:.0f}mm2(๋ณด์ ์ธก์ )\")\n",
"\n",
"# ---------------- 3) ๊น์ดยท์์๋ ๋ชจ๋ธ ----------------\n",
"dm_tr = mtr[\"depth_mm\"].values > 0.2\n",
"Xtr = np.c_[Ftr[pl[\"cols\"]].values, cross_val_predict(RandomForestClassifier(n_estimators=100, random_state=0, n_jobs=1), Ftr, mtr[\"cls\"], cv=3, method=\"predict_proba\")]\n",
"dep = GradientBoostingRegressor(n_estimators=200, max_depth=3, learning_rate=0.06, random_state=0).fit(Xtr[dm_tr], mtr[\"depth_mm\"].values[dm_tr])\n",
"d_pred = np.clip(dep.predict(np.c_[Fte[pl[\"cols\"]].values, P_te]), 0, None)\n",
"dm = mte[\"depth_mm\"].values > 0.2\n",
"print(f\"[4] ๊น์ด ์ถ์ (๊น์ด ์๋ ์ ํ {int(dm.sum())}์ฅ, ๋์ ๋ ๊น์ด 0์ผ๋ก ์ ์ธ): MAPE {mp(mte['depth_mm'].values[dm], d_pred[dm]):.1f}%, MAE {np.mean(np.abs(mte['depth_mm'].values[dm] - d_pred[dm])):.2f}mm\")\n",
"print(\" ๊น์ด๋ ์ฌ์ง ํ ์ฅ์ผ๋ก ์ง์ ๊ด์ธก๋์ง ์์ ๋ฉด์ ยท๋ช
์์์ ๊ฐ์ ์ถ์ ํ๋ฏ๋ก ์์๋๋ณด๋ค ์ค์ฐจ๊ฐ ํฌ๋ค(์ค์ ์
๋ฌด๋ ๊น์ด ๊ฒ์ด์ง ๋ฑ ํ์ฅ ์ธก์ ์ผ๋ก ํ์ธ).\")\n",
"y = mte[\"damage_idx\"].values\n",
"big = y >= 10\n",
"mape_d, mae_d = mp(y[big], idx_te[big]), float(np.mean(np.abs(y - idx_te)))\n",
"base = LinearRegression().fit(Ftr[[\"area_px\"]], mtr[\"damage_idx\"])\n",
"b_pred = np.clip(base.predict(Fte[[\"area_px\"]]), 0, 100)\n",
"print(f\"[5] ์์๋ ์ ์(0~100, ์ ์ 10 ์ด์ {int(big.sum())}์ฅ): ๋ฉด์ ๋ง ์ฐ๋ ๊ธฐ์ค MAPE {mp(y[big], b_pred[big]):.1f}% -> ์ ์ ๋ชจ๋ธ MAPE {mape_d:.1f}% (๋ชฉํ 25% ์ดํ: {'๋ฌ์ฑ' if mape_d <= 25 else '๋ฏธ๋ฌ'}), MAE {mae_d:.2f}, R2 {r2_score(y, idx_te):.3f}\")\n",
"g_true, g_pred = mte[\"grade\"].values, grade_of(idx_te)\n",
"print(f\" ์ ๋น ๋ฑ๊ธ ์ผ์น์จ {(g_true == g_pred).mean():.1%}, ํ ๋จ๊ณ ์ด๋ด {(np.abs(g_true - g_pred) <= 1).mean():.1%}\")\n",
"\n",
"# ---------------- 4) ๋ณ์๊ตฐ ์ ๊ฑฐ ์คํ ----------------\n",
"groups = {\"๋ถํ ๋ฉด์ ยท๋ชจ์\": [\"area_px\", \"area_frac\", \"n_comp\", \"big_px\", \"elong\", \"fill\", \"thin\", \"cy\", \"cx\"], \"๋ช
์(์ด๋์ด ๋น์จยท๋๋น)\": [\"dark_frac\", \"c_mean\", \"c_std\", \"c_min\", \"in_dark\", \"ring_contrast\"],\n",
" \"์(๋ถ์ ๊ฐ์)\": [\"rust_frac\", \"chroma_mean\", \"chroma_max\", \"in_chroma\", \"sat\"], \"๊ฐ์ฅ์๋ฆฌยทํ
๋๋ฆฌ\": [\"edge_mean\", \"edge_p95\", \"rim_frac\"], \"์กฐ๋ช
\": [\"gain\"]}\n",
"full_cols = list(Ftr.columns)\n",
"\n",
"\n",
"def run(cols):\n",
" g = GradientBoostingRegressor(n_estimators=200, max_depth=3, learning_rate=0.06, random_state=0).fit(Ftr[cols], mtr[\"damage_idx\"])\n",
" p = np.clip(g.predict(Fte[cols]), 0, 100)\n",
" return mp(y[big], p[big])\n",
"\n",
"\n",
"full = run(full_cols)\n",
"ab = pd.DataFrame([(k, run([c for c in full_cols if c not in v])) for k, v in groups.items()], columns=[\"์ ๊ฑฐํ ๋ณ์๊ตฐ\", \"MAPE(%)\"]).assign(์ฆ๊ฐ=lambda x: x[\"MAPE(%)\"] - full).sort_values(\"์ฆ๊ฐ\", ascending=False)\n",
"print(f\"\\n[6] ๋ณ์๊ตฐ ์ ๊ฑฐ(๋ถ๋ฅ ํ๋ฅ ์์ด 24๊ฐ ํผ์ฒ๋ง ์ฌ์ฉ, ์ ์ฒด MAPE {full:.1f}%)\\n\" + ab.round(2).to_string(index=False))\n",
"print(f\" ํด์: ์ ๊ฑฐ ์ ์ค์ฐจ๊ฐ ๊ฐ์ฅ ํฌ๊ฒ ๋์ด๋ ๋ณ์๊ตฐ์ '{ab.iloc[0]['์ ๊ฑฐํ ๋ณ์๊ตฐ']}'(+{ab.iloc[0]['์ฆ๊ฐ']:.1f}%p)์ด๋ค. ๋ณ์๊ตฐ๋ผ๋ฆฌ ์ ๋ณด๊ฐ ๊ฒน์ณ ๋จ๋
์ ๊ฑฐ ํจ๊ณผ๋ ์๊ฒ ๋ํ๋ ์ ์๋ค.\")\n",
"print(\" ํ์ ์ ์ ๋น ์์ฌ๊ฒฐ์ ๋ณด์กฐ์ด๋ฉฐ ์ต์ข
๋ฑ๊ธ์ ๊ฒ์ฌ๊ด์ด ํ์ ํ๋ค(M7). ์ฌ์งยท์ฅ๋น๋ ๋ชจ๋ ๊ต์ก์ฉ ๊ฐ์ ์๋ฃ์ด๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(1, 3, figsize=(15, 4.3))\n",
"ax[0].bar(range(len(sg)), sg[\"ํ๊ท IoU\"]); ax[0].set_xticks(range(len(sg))); ax[0].set_xticklabels([\"crack\", \"corrosion\", \"damage\", \"leak\"]); ax[0].set_title(\"Segmentation IoU by defect type\")\n",
"ax[1].scatter(a_true[m], a_raw[m], s=8, label=\"raw\"); ax[1].scatter(a_true[m], a_cal[m], s=8, label=\"calibrated\"); ax[1].plot([0, .2], [0, .2], \"k--\"); ax[1].legend(); ax[1].set_title(\"Defect area fraction: true vs measured\")\n",
"ax[2].scatter(y, idx_te, s=8); ax[2].plot([0, 100], [0, 100], \"k--\"); ax[2].set_title(\"Damage index: true vs predicted\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
04 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[1] ํ์ ํผ์ฒ 24๊ฐ(๋ถํ ๋ฉด์ ยท์ฑ๋ถ ์ยท๋ชจ์, ๋ช
์, ์, ๊ฐ์ฅ์๋ฆฌ, ์์น) / ํ์ต 600์ฅ, ํ๊ฐ 300์ฅ (1ํ์ = 1.5mm ๊ฐ์ )\n",
"[2] ๋ถํ ์ฑ๋ฅ(ํ๊ฐ 300์ฅ)\n",
"๊ฒฐํจ ์ฅ์ ํ๊ท IoU ํ๊ท Dice ๊ฒ์ถ๋ฅ \n",
"๊ท ์ด 48 0.89 0.94 1.0\n",
"๋ถ์ 77 0.74 0.84 1.0\n",
"ํ์ 57 0.61 0.71 1.0\n",
"๋์ 48 0.94 0.97 1.0\n",
" ๊ฒฐํจ ์๋ ์ฌ์ง 70์ฅ ์ค ๋ถํ ๊ฒฐ๊ณผ๊ฐ ๊ฑฐ์ ๋น์ด ์๋(12ํ์ ๋ฏธ๋ง) ์ฌ์ง 100% (๋ฌดํดํ ์ผ๋ฃฉยท์คํฌ๋์น ์คํ ์ต์ )\n",
"\n",
"[3] ๋ฉด์ ์ธก์ (๊ฒฐํจ 230์ฅ): ๋ถํ ์์ธก์ MAPE 20.8% -> ์ ํ๋ณ ์ ํ ๋ณด์ ํ 10.2% (๋ถ๋ฅ ์ ํ๋ 98.7%)\n",
" ๋ฉด์ ํ์ฐ ์: ํ๊ท ๊ฒฐํจ ๋ฉด์ 565mm2(์ ๋ต) / 548mm2(๋ณด์ ์ธก์ )\n",
"[4] ๊น์ด ์ถ์ (๊น์ด ์๋ ์ ํ 181์ฅ, ๋์ ๋ ๊น์ด 0์ผ๋ก ์ ์ธ): MAPE 28.0%, MAE 0.40mm\n",
" ๊น์ด๋ ์ฌ์ง ํ ์ฅ์ผ๋ก ์ง์ ๊ด์ธก๋์ง ์์ ๋ฉด์ ยท๋ช
์์์ ๊ฐ์ ์ถ์ ํ๋ฏ๋ก ์์๋๋ณด๋ค ์ค์ฐจ๊ฐ ํฌ๋ค(์ค์ ์
๋ฌด๋ ๊น์ด ๊ฒ์ด์ง ๋ฑ ํ์ฅ ์ธก์ ์ผ๋ก ํ์ธ).\n",
"[5] ์์๋ ์ ์(0~100, ์ ์ 10 ์ด์ 220์ฅ): ๋ฉด์ ๋ง ์ฐ๋ ๊ธฐ์ค MAPE 30.8% -> ์ ์ ๋ชจ๋ธ MAPE 8.8% (๋ชฉํ 25% ์ดํ: ๋ฌ์ฑ), MAE 3.22, R2 0.946\n",
" ์ ๋น ๋ฑ๊ธ ์ผ์น์จ 93.3%, ํ ๋จ๊ณ ์ด๋ด 99.7%\n",
"\n",
"[6] ๋ณ์๊ตฐ ์ ๊ฑฐ(๋ถ๋ฅ ํ๋ฅ ์์ด 24๊ฐ ํผ์ฒ๋ง ์ฌ์ฉ, ์ ์ฒด MAPE 10.5%)\n",
" ์ ๊ฑฐํ ๋ณ์๊ตฐ MAPE(%) ์ฆ๊ฐ\n",
" ์(๋ถ์ ๊ฐ์) 13.46 2.98\n",
" ๋ถํ ๋ฉด์ ยท๋ชจ์ 11.45 0.97\n",
"๋ช
์(์ด๋์ด ๋น์จยท๋๋น) 11.29 0.81\n",
" ์กฐ๋ช
10.67 0.19\n",
" ๊ฐ์ฅ์๋ฆฌยทํ
๋๋ฆฌ 10.60 0.12\n",
" ํด์: ์ ๊ฑฐ ์ ์ค์ฐจ๊ฐ ๊ฐ์ฅ ํฌ๊ฒ ๋์ด๋ ๋ณ์๊ตฐ์ '์(๋ถ์ ๊ฐ์)'(+3.0%p)์ด๋ค. ๋ณ์๊ตฐ๋ผ๋ฆฌ ์ ๋ณด๊ฐ ๊ฒน์ณ ๋จ๋
์ ๊ฑฐ ํจ๊ณผ๋ ์๊ฒ ๋ํ๋ ์ ์๋ค.\n",
" ํ์ ์ ์ ๋น ์์ฌ๊ฒฐ์ ๋ณด์กฐ์ด๋ฉฐ ์ต์ข
๋ฑ๊ธ์ ๊ฒ์ฌ๊ด์ด ํ์ ํ๋ค(M7). ์ฌ์งยท์ฅ๋น๋ ๋ชจ๋ ๊ต์ก์ฉ ๊ฐ์ ์๋ฃ์ด๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
05. ์ ๋น ๋ฑ๊ธ(ํ์ฅ์ ๋น/์ผ์ ์ ๋น/์ฐฝ์ ๋น) ์๋ ๋ถ๋ฅ ๋ฐ ํ์ ์ ๋ขฐ๋ ์ง์ ์ฐ์ถ ์์ง ๊ฐ๋ฐ\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ ๋น ๋ฑ๊ธ(ํ์ฅ/์ผ์ /์ฐฝ์ ๋น) ์๋ ๋ถ๋ฅ ๋ฐ ์ ๋ขฐ๋ ์ง์ ์์ง์ ๊ฐ๋ฐํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์์๋์ ๊ฒฐํจ ์ ํ ํ๋ฅ ๋ก ์ ๋น ๋ฑ๊ธ(ํ์ฅ์ ๋นยท์ผ์ ์ ๋นยท์ฐฝ์ ๋น)์ ์๋ ๋ถ๋ฅํ๊ณ ํ์ ์ ๋ขฐ๋ ์ง์๋ฅผ ์ฐ์ถํ๋ ์์ง์ ๊ฐ๋ฐํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๋ถํ ํน์ง๊ณผ ์ ํ ํ๋ฅ ๋ก ์์๋๋ฅผ ํ๊ทํ๊ณ ๊ฒฝ๊ณ๊ฐ์ผ๋ก ์ ๋น ๋ฑ๊ธ์ ์ ํด ๊ธฐ์ค ๋ชจ๋ธ(๋ฐ๊ธฐ ํ์คํ ๊ทธ๋จ ํ๊ท)๊ณผ MAEยทRMSEยทR2๋ฅผ ๋น๊ต ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/models/qlt03_advanced_model.py` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๋น์ ํ/๋ถ๊ท์น ์์์ ๋ํ ๋ชจ๋ธ์ ์ ์๋ ฅ ๋ฐ ์์ธก ์์ ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ๊ธฐ์ค ๋ชจ๋ธ ๋๋น ์ค์ฐจ์จ(MAE/RMSE) 20% ์ด์ ๊ฐ์ , R2 Score 0.85 ์ด์\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ๋ชจ๋ธ ํ์ดํผํ๋ผ๋ฏธํฐ ํ๋ ์๋ฃ ๋ฐ ์ต์ ๋ชจ๋ธ ๊ฐ์ค์น ์ ์ฅ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: Hold-out ํ
์คํธ ๋ฐ์ดํฐ์
๋์ ์ต์ข
์ฑ๋ฅ ํ๊ฐ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์์๋ ์ ์ ํ๊ท์ ๊ฒฐํจ ์ ํ ํ๋ฅ ๋ก ์ ๋น ๋ฑ๊ธ(ํ์ฅ์ ๋นยท์ผ์ ์ ๋นยท์ฐฝ์ ๋น)์ ์๋ ๋ถ๋ฅํ๊ณ , ๋ถ๋ฅ ํ๋ฅ ยท๋ฑ๊ธ ๊ฒฝ๊ณ๊น์ง์ ๊ฑฐ๋ฆฌยท์ฌ์ง ํ์ง๋ก ํ์ ์ ๋ขฐ๋ ์ง์(0~100)๋ฅผ ์ฐ์ถํ๋ ์์ง์ ๋ง๋ค์ด ๊ธฐ์ค ๋ชจ๋ธ ๋๋น MAEยทRMSE ๊ฐ์ ๊ณผ ์ ๋ขฐ๋ ๋ณด์ , ์ ์กฐ๋ยท์งํ ์ฌ์ง ๋ฑ ๋ถ๊ท์น ์ํฉ์์์ ์์ ์ฑ์ ๊ฒ์ฆํ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"from sklearn.ensemble import RandomForestClassifier, GradientBoostingRegressor\n",
"from sklearn.linear_model import LinearRegression\n",
"from sklearn.model_selection import cross_val_predict\n",
"from sklearn.metrics import r2_score, confusion_matrix\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ฅ๋น ์ธ๊ด ์ฌ์ง ์์ฑ๊ธฐ (์ค์ ์ฅ๋นยท๋ถ๋ยท์์น ์ฌ์ง์ด ์๋๋ผ ๋์๋ก ๊ทธ๋ฆฐ 64x64 ํฉ์ฑ ์ด๋ฏธ์ง)\n",
"# ๋์ฅ๋ ๊ธ์ ํ๋ฉด์ ๊ท ์ด(crack)ยท๋ถ์(corrosion)ยทํ์(damage)ยท๋์ (leak)๋ฅผ ๊ทธ๋ฆฌ๊ณ , ์กฐ๋ช
ํธ์ฐจ์ ์งํ(mud) ์ค์ผ์ ์๋๋ค.\n",
"# ์ด๋ฏธ์ง์ ํจ๊ป ์ฅ๋น ๊ด๋ฆฌ ๊ธฐ๋ก(์ ์ ์์ฝ)๊ณผ ์ ๋ต ๊ฒฐํจ ๋ง์คํฌยท์์๋ยท์ ๋น ๋ฑ๊ธ์ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"from scipy import ndimage as ndi\n",
"\n",
"SZ = 64\n",
"CLASSES = [\"none\", \"crack\", \"corrosion\", \"damage\", \"leak\"]\n",
"KO_CLS = {\"none\": \"๊ฒฐํจ ์์\", \"crack\": \"๊ท ์ด\", \"corrosion\": \"๋ถ์\", \"damage\": \"ํ์\", \"leak\": \"๋์ \"}\n",
"CLS_W = {\"none\": 0.0, \"crack\": 1.4, \"corrosion\": 1.0, \"damage\": 1.6, \"leak\": 0.6} # ์์๋ ๊ณ์ฐ์ฉ ๊ฒฐํจ ์ ํ ๊ฐ์ค์น\n",
"GRADE_NAME = {1: \"ํ์ฅ์ ๋น\", 2: \"์ผ์ ์ ๋น\", 3: \"์ฐฝ์ ๋น\"}\n",
"YY, XX = np.mgrid[0:SZ, 0:SZ]\n",
"\n",
"\n",
"def grade_of(idx):\n",
" # [TODO : ํ์ ์ค์ต 5] ์ ๋น ๋ฑ๊ธ(ํ์ฅ์ ๋น/์ผ์ ์ ๋น/์ฐฝ์ ๋น) ์๋ ๋ถ๋ฅ ๋ฐ ํ์ ์ ๋ขฐ๋ ์ง์ ์ฐ์ถ ์์ง ๊ฐ๋ฐ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def blob(rng, cy, cx, r, rough=0.35):\n",
" \"\"\"๋ถ๊ท์นํ ์ํ ์์ญ ๋ง์คํฌ.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 5] ์ ๋น ๋ฑ๊ธ(ํ์ฅ์ ๋น/์ผ์ ์ ๋น/์ฐฝ์ ๋น) ์๋ ๋ถ๋ฅ ๋ฐ ํ์ ์ ๋ขฐ๋ ์ง์ ์ฐ์ถ ์์ง ๊ฐ๋ฐ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def draw(rng, cls, sev):\n",
" \"\"\"๊ฒฐํจ ์ ํยท์ฌ๊ฐ๋ sev(0~1)์ ๋ง๋ ์ ๋ต ๋ง์คํฌ์ ์ ์ ๋ณด๋ฅผ ๋ง๋ ๋ค.\"\"\"\n",
" m = np.zeros((SZ, SZ), bool)\n",
" if cls == \"crack\":\n",
" y, x, ang = rng.uniform(12, 52), rng.uniform(12, 52), rng.uniform(0, np.pi)\n",
" for _ in range(int(20 + 40 * sev)):\n",
" ang += rng.normal(0, 0.35); y += np.sin(ang) * 1.2; x += np.cos(ang) * 1.2\n",
" if 0 <= int(y) < SZ and 0 <= int(x) < SZ:\n",
" m[int(y), int(x)] = True\n",
" m = ndi.binary_dilation(m, iterations=1 + int(sev > 0.6))\n",
" elif cls == \"corrosion\":\n",
" for _ in range(int(rng.integers(1, 4))):\n",
" m |= blob(rng, rng.uniform(14, 50), rng.uniform(14, 50), 3 + 9 * sev * rng.uniform(0.6, 1.1))\n",
" elif cls == \"damage\":\n",
" m = blob(rng, rng.uniform(20, 44), rng.uniform(20, 44), 6 + 12 * sev, 0.3)\n",
" elif cls == \"leak\":\n",
" x0, y0 = rng.uniform(16, 48), rng.uniform(8, 24)\n",
" w, ln = 2 + 4 * sev, 12 + 26 * sev\n",
" m = (np.abs(XX - x0 - 0.15 * (YY - y0)) < w * (1 - 0.4 * (YY - y0) / ln)) & (YY > y0) & (YY < y0 + ln)\n",
" return m\n",
"\n",
"\n",
"def make_images(n=600, seed=0, mud=0.0, p_none=0.25, dark=False):\n",
" \"\"\"๊ฐ์ ์ธ๊ด ์ฌ์ง n์ฅ(+์งํ ์ค์ผ mud ๋น์จ)๊ณผ ๊ด๋ฆฌ ๊ธฐ๋กยท์ ๋ต. ๋ฐํ: meta(DataFrame), imgs(n,64,64,3), masks(n,64,64).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" imgs, masks, rows = np.zeros((n, SZ, SZ, 3), np.float32), np.zeros((n, SZ, SZ), bool), []\n",
" for i in range(n):\n",
" cls = \"none\" if rng.random() < p_none else str(rng.choice(CLASSES[1:], p=[0.25, 0.3, 0.25, 0.2]))\n",
" sev = 0.0 if cls == \"none\" else float(rng.beta(1.6, 2.0))\n",
" base = np.array([0.42, 0.46, 0.40]) * (1 + 0.5 * ndi.gaussian_filter(rng.normal(size=(SZ, SZ)), 8)[..., None] * 3)\n",
" img = base + rng.normal(0, 0.012, (SZ, SZ, 3))\n",
" m = draw(rng, cls, sev)\n",
" a = ndi.gaussian_filter(m.astype(float), 0.8)[..., None] # ๊ฒฝ๊ณ๋ฅผ ๋ถ๋๋ฝ๊ฒ ์๋๋ค\n",
" if cls == \"crack\":\n",
" col = img * rng.uniform(0.2, 0.45)\n",
" elif cls == \"corrosion\":\n",
" col = np.array([0.55, 0.30, 0.12]) * rng.uniform(0.75, 1.15) * (0.85 + 0.3 * rng.random((SZ, SZ, 1)))\n",
" elif cls == \"damage\":\n",
" col = np.ones_like(img) * np.array([0.34, 0.34, 0.36]) * (1.0 - 0.45 * sev) * rng.uniform(0.75, 1.0)\n",
" rim = m & ~ndi.binary_erosion(m, iterations=2)\n",
" a = np.maximum(a, 0.0); img = np.where(rim[..., None], 0.7, img)\n",
" elif cls == \"leak\":\n",
" col = np.ones_like(img) * np.array([0.10, 0.09, 0.08]) * rng.uniform(0.8, 2.6)\n",
" else:\n",
" col = img\n",
" img = img * (1 - a) + col * a\n",
" if rng.random() < 0.35: # ๋ฌดํดํ ์ผ๋ฃฉ(๊ฒฐํจ ์๋): ์
์ ๊ฐ์ ๋ฒ์ง\n",
" st = blob(rng, rng.uniform(5, 58), rng.uniform(5, 58), rng.uniform(6, 14), 0.5)\n",
" al = ndi.gaussian_filter(st.astype(float), 2.5)[..., None] * rng.uniform(0.15, 0.4)\n",
" img = img * (1 - al) + np.array([0.50, 0.40, 0.30]) * al\n",
" if rng.random() < 0.3: # ๋ฌดํดํ ๊ธด ์คํฌ๋์น(๋ฐ์ ์ )\n",
" y0, x0, ang = rng.uniform(5, 58), rng.uniform(5, 58), rng.uniform(0, np.pi)\n",
" for t in np.linspace(0, 25, 40):\n",
" yy, xx = int(y0 + t * np.sin(ang)), int(x0 + t * np.cos(ang))\n",
" if 0 <= yy < SZ and 0 <= xx < SZ:\n",
" img[yy, xx] = np.clip(img[yy, xx] * 1.5, 0, 1)\n",
" if mud > 0: # ์งํ ์ค์ผ: ๊ฒฐํจ ์๋ฅผ ๋ฎ์ ์ ์๋ค(์ ๋ต ๋ง์คํฌ๋ ๊ทธ๋๋ก)\n",
" mm = np.zeros((SZ, SZ), bool)\n",
" while mm.mean() < mud:\n",
" mm |= blob(rng, rng.uniform(0, SZ), rng.uniform(0, SZ), rng.uniform(5, 11), 0.5)\n",
" img[mm] = np.array([0.38, 0.30, 0.22]) * (0.85 + 0.3 * rng.random((mm.sum(), 1)))\n",
" gain = rng.uniform(0.3, 0.55) if dark else rng.uniform(0.5, 1.15)\n",
" ang = rng.uniform(0, 2 * np.pi)\n",
" light = gain * (1 + 0.3 * (np.cos(ang) * (XX - 32) + np.sin(ang) * (YY - 32)) / 32)\n",
" imgs[i] = np.clip(img * light[..., None] + rng.normal(0, 0.025, (SZ, SZ, 3)), 0, 1)\n",
" masks[i] = m\n",
" area = float(m.mean())\n",
" depth = {\"none\": 0.0, \"crack\": 0.3 + 3.0 * sev, \"corrosion\": 0.1 + 1.5 * sev, \"damage\": 1.0 + 8.0 * sev, \"leak\": 0.0}[cls] * rng.lognormal(0, 0.1)\n",
" idx = float(np.clip(100 * (1 - np.exp(-(CLS_W[cls] * area * 9 + 0.05 * depth))) + rng.normal(0, 1.5), 0, 100))\n",
" rows.append({\"img_id\": f\"IMG{seed:02d}{i:04d}\", \"cls\": cls, \"sev\": sev, \"area_frac\": area, \"depth_mm\": depth, \"damage_idx\": idx,\n",
" \"grade\": int(grade_of(idx)), \"gain\": gain, \"mud\": mud})\n",
" meta = pd.DataFrame(rows)\n",
" # ์ฅ๋น ๊ด๋ฆฌ ๊ธฐ๋ก(์ ์ ์์ฝ): ์ฌ์ง ํ์ ๊ณผ ๋์กฐํ ๊ฐ\n",
" age = rng.integers(1, 25, n)\n",
" meta[\"day\"] = np.sort(rng.integers(0, 90, n))\n",
" meta[\"equip_type\"] = rng.choice([\"vehicle\", \"generator\", \"comm\", \"trailer\"], n, p=[0.4, 0.2, 0.2, 0.2])\n",
" meta[\"mfg_year\"] = 2026 - age\n",
" meta[\"op_hours\"] = np.clip(rng.normal(180 * age, 400, n) + 8 * meta[\"damage_idx\"], 10, 20000)\n",
" meta[\"env\"] = rng.choice([\"coastal\", \"inland\"], n, p=[0.3, 0.7])\n",
" reg = np.clip(meta[\"grade\"] + rng.choice([-1, 0, 0, 0, 0, 0, 1, 2], n), 1, 3) # ์ฅ๋น ๊ด๋ฆฌ ๊ธฐ๋ก์ ์ง์ ์ ๋น ๋ฑ๊ธ\n",
" meta[\"reg_grade\"] = reg\n",
" meta[\"emergency\"] = rng.random(n) < 0.04 # ๊ธด๊ธ ์ ๊ฒ ์์ฒญ(ํ์ง ๋ถ๋์ด์ด๋ ๋ณด์กด)\n",
" return meta, imgs, masks\n",
"# ---- ์ ์ฒ๋ฆฌ(์กฐ๋ช
๋ณด์ ยทํ์คํ)์ ๊ฒฐํจ ํ๋ณด ๋ถํ , ํน์ง ์ถ์ถ (cv2 ๋์ numpyยทscipy ์ฌ์ฉ) ----\n",
"def correct(img):\n",
" \"\"\"์กฐ๋ช
๋ณด์ : ๋ฐ๊ธฐ์ ํ๋ฉด(์กฐ๋ช
๊ธฐ์ธ๊ธฐ)์ ์ ํฉํด ๋๋๋ค. ์ด๋์ด ๊ฒฐํจ ํ์๋ ์ ์ธํ๊ณ ๋ ๋ฒ ๋ฐ๋ณต ์ ํฉํ๋ค. ๋ฐํ: ๋ณด์ ๋ฐ๊ธฐ(์ ์ ๋ฉด ์ฝ 1), ์ ๋น์จ.\"\"\"\n",
" g = img.mean(axis=2)\n",
" A = np.c_[np.ones(SZ * SZ), XX.ravel() / SZ, YY.ravel() / SZ]\n",
" keep = np.ones(SZ * SZ, bool)\n",
" for _ in range(3):\n",
" coef, *_ = np.linalg.lstsq(A[keep], g.ravel()[keep], rcond=None)\n",
" plane = (A @ coef).reshape(SZ, SZ)\n",
" keep = (np.abs(g - plane) < 0.08 * np.maximum(plane, 1e-3)).ravel()\n",
" if keep.sum() < 200:\n",
" keep[:] = True\n",
" c = g / np.maximum(plane, 1e-3)\n",
" chroma = img[..., 0] / (img.sum(axis=2) + 1e-6)\n",
" return c, chroma\n",
"\n",
"\n",
"SEGP = {\"dark\": 0.7, \"rust\": 0.5, \"rim\": 1.45} # ๋ถํ ์๊ณ๊ฐ(์ด๋์ ๋ณด์ ๋ฐ๊ธฐ, ๋ถ์ ๋น์จ, ๋ฐ์ ํ
๋๋ฆฌ): ์ฌ๋ณด์ ๋์\n",
"\n",
"\n",
"def segment(img):\n",
" \"\"\"๊ฒฐํจ ํ๋ณด ๋ง์คํฌ: ์ด๋์ด ์์ญ(๊ท ์ดยท๋์ ยทํ์) + ๋ถ์ ๊ฐ์ ์์ญ(๋ถ์) + ๋ฐ์ ํ
๋๋ฆฌ(ํ์).\"\"\"\n",
" c, chroma = correct(img)\n",
" dark = c < SEGP[\"dark\"]\n",
" rust = (chroma > SEGP[\"rust\"]) & (c > 0.45)\n",
" rim = c > SEGP[\"rim\"]\n",
" m = dark | rust | ndi.binary_dilation(rim, iterations=1) & ndi.binary_dilation(dark, iterations=3)\n",
" m = ndi.binary_opening(m, structure=np.ones((2, 2))) if m.sum() > 12 else m\n",
" return m, c, chroma, dark, rust, rim\n",
"\n",
"\n",
"def extract(img):\n",
" \"\"\"๋ถํ ๊ฒฐ๊ณผ์ ์ยท๋ชจ์์์ 24๊ฐ ํน์ง์ ๋ง๋ ๋ค.\"\"\"\n",
" m, c, chroma, dark, rust, rim = segment(img)\n",
" lab, n = ndi.label(m)\n",
" area = float(m.sum())\n",
" if n:\n",
" sizes = ndi.sum(m, lab, range(1, n + 1))\n",
" k = int(np.argmax(sizes)) + 1\n",
" sl = ndi.find_objects(lab)[k - 1]\n",
" h, w = sl[0].stop - sl[0].start, sl[1].stop - sl[1].start\n",
" big = float(sizes.max())\n",
" elong, fill = max(h, w) / max(min(h, w), 1), big / (h * w)\n",
" else:\n",
" big, elong, fill = 0.0, 1.0, 0.0\n",
" gy, gx = np.gradient(c)\n",
" mag = np.hypot(gx, gy)\n",
" inside = m.sum() > 0\n",
" return {\"area_px\": area, \"area_frac\": area / SZ ** 2, \"n_comp\": float(n), \"big_px\": big, \"elong\": elong, \"fill\": fill,\n",
" \"dark_frac\": float(dark.mean()), \"rust_frac\": float(rust.mean()), \"rim_frac\": float(rim.mean()),\n",
" \"c_mean\": float(c.mean()), \"c_std\": float(c.std()), \"c_min\": float(c.min()), \"edge_mean\": float(mag.mean()), \"edge_p95\": float(np.quantile(mag, 0.95)),\n",
" \"chroma_mean\": float(chroma.mean()), \"chroma_max\": float(chroma.max()),\n",
" \"in_dark\": float(c[m].mean()) if inside else 1.0, \"in_chroma\": float(chroma[m].mean()) if inside else float(chroma.mean()),\n",
" \"ring_contrast\": float(c[ndi.binary_dilation(m, iterations=3) & ~m].mean() - c[m].mean()) if inside and (~m).any() else 0.0,\n",
" \"thin\": float(1 - fill), \"gain\": float(img.mean()), \"sat\": float((img.max(axis=2) - img.min(axis=2)).mean()),\n",
" \"cy\": float(ndi.center_of_mass(m)[0] / SZ) if inside else 0.5, \"cx\": float(ndi.center_of_mass(m)[1] / SZ) if inside else 0.5}\n",
"\n",
"\n",
"def feature_table(imgs):\n",
" return pd.DataFrame([extract(im) for im in imgs])\n",
"# ---- ํ์ ํ์ดํ๋ผ์ธ: ๊ฒฐํจ ์ ํ ๋ถ๋ฅ + ์์๋ ํ๊ท(๋ถ๋ฅ ํ๋ฅ ํฌํจ) -> ์ ๋น ๋ฑ๊ธ ----\n",
"def fit_pipeline(meta, F):\n",
" \"\"\"๊ฒฐํจ ์ ํ ๋ถ๋ฅ๊ธฐ์ ์์๋ ํ๊ท๊ธฐ๋ฅผ ํ์ตํ๋ค. ํ๊ท๊ธฐ ์
๋ ฅ์ ๋ถ๋ฅ ํ๋ฅ ์ ๊ต์ฐจ ์์ธก์ผ๋ก ๋ง๋ค์ด ๋์ค์ ๋ง๋๋ค.\"\"\"\n",
" clf = RandomForestClassifier(n_estimators=150, random_state=0, n_jobs=1)\n",
" P = cross_val_predict(clf, F, meta[\"cls\"], cv=4, method=\"predict_proba\")\n",
" clf.fit(F, meta[\"cls\"])\n",
" reg = GradientBoostingRegressor(n_estimators=200, max_depth=3, learning_rate=0.06, random_state=0).fit(np.c_[F.values, P], meta[\"damage_idx\"])\n",
" return {\"clf\": clf, \"reg\": reg, \"cols\": list(F.columns)}\n",
"\n",
"\n",
"def predict_pipeline(pl, F):\n",
" X = F[pl[\"cols\"]]\n",
" P = pl[\"clf\"].predict_proba(X)\n",
" idx = np.clip(pl[\"reg\"].predict(np.c_[X.values, P]), 0, 100)\n",
" return pl[\"clf\"].classes_[P.argmax(axis=1)], P, idx, P.max(axis=1)\n",
"# ---- ์ฌ์ง ํ์ง ๊ฒ์ดํธ: ์งํยท์ค์ผ ๊ฐ๋ฆผ ์ถ์ ๊ณผ ํ์ ์ ๋ขฐ๋ ์ง์ ----\n",
"def mud_est(img):\n",
" \"\"\"์งํ(๊ฐ์, ์ฑ๋ ๋ฎ์ ๋ถ์ ๊ธฐ) ํ์ ๋น์จ์ ์ถ์ ํ๋ค. 3x3 ํ๊ท ์ผ๋ก ์ก์์ ์ค์ธ ๋ค ์ ๋น์จ๋ก ํ๋จํ๋ฉฐ, ์ค์ ๊ฐ๋ฆผ ๋น์จ๋ณด๋ค ์๊ฒ ๋์ค์ง๋ง ์์๋ ์ ์ง๋๋ค.\"\"\"\n",
" sm = ndi.uniform_filter(img, size=(3, 3, 1))\n",
" s = sm.sum(axis=2) + 1e-6\n",
" rr, gg = sm[..., 0] / s, sm[..., 1] / s\n",
" return float(((rr > 0.385) & (rr < 0.46) & (gg < 0.325)).mean())\n",
"\n",
"\n",
"def confidence(P, idx, F, mud):\n",
" \"\"\"ํ์ ์ ๋ขฐ๋ ์ง์ 0~100 = 50 x ๋ถ๋ฅ ํ๋ฅ ์ต๋๊ฐ + 35 x ๋ฑ๊ธ ๊ฒฝ๊ณ๊น์ง ๊ฑฐ๋ฆฌ(8์ ์์ ํฌํ) + 15 x ์ฌ์ง ํ์ง. ์ค์ผ ์์ฌ(mud ์ถ์ 1% ์ด์)์ด๋ฉด ์ํ 75๋ก ์ ํํ๋ค.\"\"\"\n",
" bd = np.minimum(np.abs(idx - 35), np.abs(idx - 65))\n",
" qual = np.clip(1.0 - np.clip(0.55 - F[\"gain\"].values, 0, None) * 2 - np.clip(0.9 - F[\"c_mean\"].values, 0, None), 0, 1)\n",
" c = 50 * P.max(axis=1) + 35 * np.clip(bd / 8, 0, 1) + 15 * qual\n",
" return np.where(np.asarray(mud) >= 0.01, np.minimum(c, 75.0), c)\n",
"\n",
"mtr, itr, _ = make_images(600, seed=1)\n",
"mte, ite, _ = make_images(300, seed=2)\n",
"Ftr, Fte = feature_table(itr), feature_table(ite)\n",
"pl = fit_pipeline(mtr, Ftr)\n",
"\n",
"\n",
"def hist_feat(imgs):\n",
" \"\"\"๊ธฐ์ค ๋ชจ๋ธ(M3) ํน์ง: ๋ฐ๊ธฐ ํ์คํ ๊ทธ๋จ 16์นธ + ์ฑ๋ ํ๊ท ๋น์จ 3 + ๋ณ๋๊ณ์ 1.\"\"\"\n",
" out = []\n",
" for im in imgs:\n",
" g = im.mean(axis=2)\n",
" h, _ = np.histogram(g / max(g.max(), 1e-3), bins=16, range=(0, 1))\n",
" out.append(np.r_[h / g.size, im.mean(axis=(0, 1)) / max(g.mean(), 1e-3), g.std() / max(g.mean(), 1e-3)])\n",
" return np.array(out)\n",
"\n",
"\n",
"base = LinearRegression().fit(hist_feat(itr), mtr[\"damage_idx\"])\n",
"rm = lambda y, p: float(np.sqrt(np.mean((np.asarray(y) - np.asarray(p)) ** 2)))\n",
"\n",
"\n",
"# ---------------- 1) ํ๊ฐ 300์ฅ: ๊ธฐ์ค ๋ชจ๋ธ vs ์ ์ ์์ง ----------------\n",
"cls, P, idx, _ = predict_pipeline(pl, Fte)\n",
"y = mte[\"damage_idx\"].values\n",
"bp = np.clip(base.predict(hist_feat(ite)), 0, 100)\n",
"tab = pd.DataFrame({\"๊ธฐ์ค(๋ฐ๊ธฐ ํ์คํ ๊ทธ๋จ ํ๊ท)\": [np.mean(np.abs(y - bp)), rm(y, bp), r2_score(y, bp)], \"์ ์(๋ถํ ํน์ง+์ ํ ํ๋ฅ )\": [np.mean(np.abs(y - idx)), rm(y, idx), r2_score(y, idx)]}, index=[\"MAE\", \"RMSE\", \"R2\"])\n",
"print(\"[1] ์์๋ ์ ์ ์์ธก ์ฑ๋ฅ(ํ๊ฐ 300์ฅ)\\n\" + tab.round(3).to_string())\n",
"b, e = tab.iloc[:, 0], tab.iloc[:, 1]\n",
"ok = 1 - e[\"MAE\"] / b[\"MAE\"] >= .2 and 1 - e[\"RMSE\"] / b[\"RMSE\"] >= .2\n",
"print(f\" MAE ๊ฐ์ {1 - e['MAE'] / b['MAE']:.1%}, RMSE ๊ฐ์ {1 - e['RMSE'] / b['RMSE']:.1%} (๋ชฉํ 20% ์ด์: {'๋ฌ์ฑ' if ok else '๋ฏธ๋ฌ'}), R2 {e['R2']:.3f} (๋ชฉํ 0.85 ์ด์: {'๋ฌ์ฑ' if e['R2'] >= .85 else '๋ฏธ๋ฌ'})\")\n",
"\n",
"# ---------------- 2) ์ ๋น ๋ฑ๊ธ ๋ถ๋ฅ ----------------\n",
"g_true, g_pred = mte[\"grade\"].values, grade_of(idx)\n",
"cm = pd.DataFrame(confusion_matrix(g_true, g_pred, labels=[1, 2, 3]), index=[f\"์ค์ {GRADE_NAME[i]}\" for i in (1, 2, 3)], columns=[f\"ํ์ {GRADE_NAME[i]}\" for i in (1, 2, 3)])\n",
"under = int(((g_pred < g_true)).sum())\n",
"print(f\"\\n[2] ์ ๋น ๋ฑ๊ธ ํ์ : ์ผ์น์จ {(g_true == g_pred).mean():.1%}, ํ ๋จ๊ณ ์ด๋ด {(np.abs(g_true - g_pred) <= 1).mean():.1%}, ๊ณผ์ ํ์ (์ค์ ๋ณด๋ค ๋ฎ๊ฒ) {under}์ฅ, ๊ณผ๋ ํ์ {int((g_pred > g_true).sum())}์ฅ\\n\" + cm.to_string())\n",
"print(\" ์ฐฝ์ ๋น๋ฅผ ์ผ์ ยทํ์ฅ์ผ๋ก ๋ฎ์ถ๋ ๊ณผ์ ํ์ ์ด ๊ฐ์ฅ ์ํํ๋ฏ๋ก ๊ฒฝ๊ณ ๋ถ๊ทผ์ ์ ๋ขฐ๋๋ก ๊ฑธ๋ฌ ๊ฒ์ฌ๊ด์ด ํ์ธํ๋ค.\")\n",
"\n",
"# ---------------- 3) ํ์ ์ ๋ขฐ๋ ์ง์ ๋ณด์ ----------------\n",
"mud_te = np.array([mud_est(im) for im in ite])\n",
"conf = confidence(P, idx, Fte, mud_te)\n",
"bins = pd.cut(conf, [0, 70, 80, 90, 101], labels=[\"70 ๋ฏธ๋ง\", \"70~80\", \"80~90\", \"90 ์ด์\"])\n",
"cal = pd.DataFrame({\"bin\": bins, \"ok\": g_true == g_pred, \"near\": np.abs(g_true - g_pred) <= 1}).groupby(\"bin\", observed=True).agg(๊ฑด์=(\"ok\", \"size\"), ์ผ์น์จ=(\"ok\", \"mean\"), ํ๋จ๊ณ์ด๋ด=(\"near\", \"mean\"))\n",
"print(\"\\n[3] ์ ๋ขฐ๋ ์ง์ ๊ตฌ๊ฐ๋ณ ์ค์ ์ผ์น์จ(๋์ ์ ๋ขฐ๋์ผ์๋ก ๋์์ผ ํ๋ค)\\n\" + cal.round(3).to_string())\n",
"hi = conf >= 80\n",
"print(f\" ์ ๋ขฐ๋ 80 ์ด์ ์๋ ํต๊ณผ {hi.sum()}์ฅ({hi.mean():.0%}): ์ผ์น์จ {(g_true == g_pred)[hi].mean():.1%} / 80 ๋ฏธ๋ง {(~hi).sum()}์ฅ์ ๊ฒ์ฌ๊ด ํ์ธ ๋์(์ผ์น์จ {(g_true == g_pred)[~hi].mean():.1%})\")\n",
"\n",
"# ---------------- 4) ๋ถ๊ท์น ์ํฉ: ์ ์กฐ๋, ์งํ ๊ฐ๋ฆผ, ์์ ๊ฒฐํจ ----------------\n",
"rows = []\n",
"for name, kw in [(\"์ ์ ์กฐ๋ช
\", dict(seed=2)), (\"์ ์กฐ๋\", dict(seed=7, dark=True)), (\"์งํ 20% ๊ฐ๋ฆผ\", dict(seed=8, mud=0.2)), (\"์งํ 35% ๊ฐ๋ฆผ\", dict(seed=9, mud=0.35))]:\n",
" mm, ii = (mte, ite) if name == \"์ ์ ์กฐ๋ช
\" else make_images(150, **kw)[:2]\n",
" FF = Fte if name == \"์ ์ ์กฐ๋ช
\" else feature_table(ii)\n",
" c2, P2, i2, _ = predict_pipeline(pl, FF)\n",
" cf = confidence(P2, i2, FF, [mud_est(im) for im in ii])\n",
" yy = mm[\"damage_idx\"].values\n",
" gg, gp = mm[\"grade\"].values, grade_of(i2)\n",
" rows.append((name, len(mm), np.mean(np.abs(yy - np.clip(base.predict(hist_feat(ii)), 0, 100))), np.mean(np.abs(yy - i2)), (gg == gp).mean(), cf.mean(), (gg == gp)[cf >= 80].mean() if (cf >= 80).any() else np.nan, (cf >= 80).mean()))\n",
"ir = pd.DataFrame(rows, columns=[\"์กฐ๊ฑด\", \"์ฅ์\", \"๊ธฐ์ค MAE\", \"์ ์ MAE\", \"๋ฑ๊ธ ์ผ์น์จ\", \"ํ๊ท ์ ๋ขฐ๋\", \"์ ๋ขฐ๋ 80 ์ด์ ์ผ์น์จ\", \"80 ์ด์ ๋น์จ\"])\n",
"print(\"\\n[4] ๋ถ๊ท์น ์ํฉ๋ณ ์ฑ๋ฅ(- ๋ ํด๋น ์ฌ์ง ์์)\\n\" + ir.round(2).to_string(index=False, na_rep=\"-\"))\n",
"print(\" ์งํ ๊ฐ๋ฆผ์ด ์ปค์ง๋ฉด ์ค์ฐจ์ ๊ณผ์ ํ์ ์ด ๋๊ณ ํ๊ท ์ ๋ขฐ๋๊ฐ ๋จ์ด์ง๋ค. ์ด๋ฐ ์ฌ์ง์ ์ฌ์ดฌ์ ์๊ตฌ ๋๋ ๊ฒ์ฌ๊ด ํ์ธ์ผ๋ก ๋ณด๋ธ๋ค.\")\n",
"print(\" ๋ฑ๊ธ์ ์ ๋น ์์์ 1์ฐจ ํ์ ์ด๋ฉฐ, ์ต์ข
์ ๋น ๋ฑ๊ธ์ ๊ฒ์ฌ๊ด์ด ํ์ ํ๋ค(M7). ์ฌ์ง์ ๊ต์ก์ฉ ๊ฐ์ ์๋ฃ์ด๋ค.\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
05 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[1] ์์๋ ์ ์ ์์ธก ์ฑ๋ฅ(ํ๊ฐ 300์ฅ)\n",
" ๊ธฐ์ค(๋ฐ๊ธฐ ํ์คํ ๊ทธ๋จ ํ๊ท) ์ ์(๋ถํ ํน์ง+์ ํ ํ๋ฅ )\n",
"MAE 12.107 3.223\n",
"RMSE 15.045 6.759\n",
"R2 0.732 0.946\n",
" MAE ๊ฐ์ 73.4%, RMSE ๊ฐ์ 55.1% (๋ชฉํ 20% ์ด์: ๋ฌ์ฑ), R2 0.946 (๋ชฉํ 0.85 ์ด์: ๋ฌ์ฑ)\n",
"\n",
"[2] ์ ๋น ๋ฑ๊ธ ํ์ : ์ผ์น์จ 93.3%, ํ ๋จ๊ณ ์ด๋ด 99.7%, ๊ณผ์ ํ์ (์ค์ ๋ณด๋ค ๋ฎ๊ฒ) 16์ฅ, ๊ณผ๋ ํ์ 4์ฅ\n",
" ํ์ ํ์ฅ์ ๋น ํ์ ์ผ์ ์ ๋น ํ์ ์ฐฝ์ ๋น\n",
"์ค์ ํ์ฅ์ ๋น 182 2 0\n",
"์ค์ ์ผ์ ์ ๋น 11 47 2\n",
"์ค์ ์ฐฝ์ ๋น 1 4 51\n",
" ์ฐฝ์ ๋น๋ฅผ ์ผ์ ยทํ์ฅ์ผ๋ก ๋ฎ์ถ๋ ๊ณผ์ ํ์ ์ด ๊ฐ์ฅ ์ํํ๋ฏ๋ก ๊ฒฝ๊ณ ๋ถ๊ทผ์ ์ ๋ขฐ๋๋ก ๊ฑธ๋ฌ ๊ฒ์ฌ๊ด์ด ํ์ธํ๋ค.\n",
"\n",
"[3] ์ ๋ขฐ๋ ์ง์ ๊ตฌ๊ฐ๋ณ ์ค์ ์ผ์น์จ(๋์ ์ ๋ขฐ๋์ผ์๋ก ๋์์ผ ํ๋ค)\n",
" ๊ฑด์ ์ผ์น์จ ํ๋จ๊ณ์ด๋ด\n",
"bin \n",
"70 ๋ฏธ๋ง 35 0.543 1.000\n",
"70~80 31 0.871 0.968\n",
"80~90 80 1.000 1.000\n",
"90 ์ด์ 154 1.000 1.000\n",
" ์ ๋ขฐ๋ 80 ์ด์ ์๋ ํต๊ณผ 234์ฅ(78%): ์ผ์น์จ 100.0% / 80 ๋ฏธ๋ง 66์ฅ์ ๊ฒ์ฌ๊ด ํ์ธ ๋์(์ผ์น์จ 69.7%)\n",
"\n",
"[4] ๋ถ๊ท์น ์ํฉ๋ณ ์ฑ๋ฅ(- ๋ ํด๋น ์ฌ์ง ์์)\n",
" ์กฐ๊ฑด ์ฅ์ ๊ธฐ์ค MAE ์ ์ MAE ๋ฑ๊ธ ์ผ์น์จ ํ๊ท ์ ๋ขฐ๋ ์ ๋ขฐ๋ 80 ์ด์ ์ผ์น์จ 80 ์ด์ ๋น์จ\n",
" ์ ์ ์กฐ๋ช
300 12.11 3.22 0.93 86.19 1.0 0.78\n",
" ์ ์กฐ๋ 150 13.17 5.52 0.93 74.60 1.0 0.45\n",
"์งํ 20% ๊ฐ๋ฆผ 150 53.72 23.89 0.45 61.91 - 0.00\n",
"์งํ 35% ๊ฐ๋ฆผ 150 66.17 19.41 0.57 59.80 - 0.00\n",
" ์งํ ๊ฐ๋ฆผ์ด ์ปค์ง๋ฉด ์ค์ฐจ์ ๊ณผ์ ํ์ ์ด ๋๊ณ ํ๊ท ์ ๋ขฐ๋๊ฐ ๋จ์ด์ง๋ค. ์ด๋ฐ ์ฌ์ง์ ์ฌ์ดฌ์ ์๊ตฌ ๋๋ ๊ฒ์ฌ๊ด ํ์ธ์ผ๋ก ๋ณด๋ธ๋ค.\n",
" ๋ฑ๊ธ์ ์ ๋น ์์์ 1์ฐจ ํ์ ์ด๋ฉฐ, ์ต์ข
์ ๋น ๋ฑ๊ธ์ ๊ฒ์ฌ๊ด์ด ํ์ ํ๋ค(M7). ์ฌ์ง์ ๊ต์ก์ฉ ๊ฐ์ ์๋ฃ์ด๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
06. ์ฅ๋น ๊ฒฐํจ ์ฌ์ง ์
๋ก๋ ๋ฐ ์์ ๋ถ์ ์๊ฐํ ํ์ ์ธํฐํ์ด์ค ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ฅ๋น ๊ฒฐํจ ์ฌ์ง ์
๋ก๋ ๋ฐ ์์ ๋ถ์ ์๊ฐํ ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ฅ๋น ๊ฒฐํจ ์ฌ์ง์ ์
๋ก๋ํ๋ฉด ์์ ๋ถ์๋ฅผ ์๊ฐํํด ํ์ ๊ณผ ๊ทผ๊ฑฐ๋ฅผ ๋ณด์ฌ ์ฃผ๋ ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์
๋ก๋ ์ฌ์ง ํ ์ฅ์์ ๊ฒฐํจ ์ ํ, ๋ฉด์ (mm2), ๊น์ด, ์์๋, ์ ๋น ๋ฑ๊ธ, ์ ๋ขฐ๋์ ์์ ๋ถ์ ์ค๊ณฝยท๋ฐ์ค๋ฅผ ๊ณ์ฐํด ํ๋ฉด์ ๊ฒน์ณ ํ์ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/explainability/qlt03_xai_explainer.py ๋ฐ XAI ๋ฆฌํฌํธ` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๊ตฐ์ ์ฅ๊ต/๊ตฐ๋ฌด์์ด ์ ๋ขฐํ๊ณ ์กฐ์นํ ์ ์๋ ์ค๋ช
์ ๋ช
ํ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ์ฃผ์ ์์ธก ๊ฑด๋น ๊ธฐ์ฌ๋ ์์ 3๊ฐ ์ธ์ ์๋ ์ฐ์ถ๋ฅ 100%\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: SHAP summary plot ๋ฐ waterfall plot ์์ฑ ํ์ดํ๋ผ์ธ ์์ฑ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ์ค๋ฌด์ ๋์ ์ค๋ช
๊ทผ๊ฑฐ ์ ์ฉ์ฑ ํ๊ฐ ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์ฅ๋น ๊ฒฐํจ ์ฌ์ง์ ์
๋ก๋ํ๋ฉด ๊ฒฐํจ ์ ํยท๋ฉด์ ยท๊น์ดยท์์๋ยท์ ๋น ๋ฑ๊ธยท์ ๋ขฐ๋์ ์์ ๋ถ์(๋ฐ์คยท์ค๊ณฝ)๋ฅผ ์๊ฐํํด ๋ณด์ฌ ์ฃผ๋ ํ์ ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํ๊ณ , ํ์ ๊ฑด๋ง๋ค ๊ทผ๊ฑฐ ๋ณ์๊ตฐ ๊ธฐ์ฌ๋ ์์ 3๊ฐ๋ฅผ ์๋ ์ฐ์ถํด ์์ฐ์ด ์ค๋ช
์ผ๋ก ์ ๊ณตํ๋ค(์๋ ํ์ ์์ด ๊ฒ์ฌ ๋๊ธฐ๋ก ๋ฑ๋ก).\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.ensemble import RandomForestClassifier, GradientBoostingRegressor\n",
"from sklearn.model_selection import cross_val_predict\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ฅ๋น ์ธ๊ด ์ฌ์ง ์์ฑ๊ธฐ (์ค์ ์ฅ๋นยท๋ถ๋ยท์์น ์ฌ์ง์ด ์๋๋ผ ๋์๋ก ๊ทธ๋ฆฐ 64x64 ํฉ์ฑ ์ด๋ฏธ์ง)\n",
"# ๋์ฅ๋ ๊ธ์ ํ๋ฉด์ ๊ท ์ด(crack)ยท๋ถ์(corrosion)ยทํ์(damage)ยท๋์ (leak)๋ฅผ ๊ทธ๋ฆฌ๊ณ , ์กฐ๋ช
ํธ์ฐจ์ ์งํ(mud) ์ค์ผ์ ์๋๋ค.\n",
"# ์ด๋ฏธ์ง์ ํจ๊ป ์ฅ๋น ๊ด๋ฆฌ ๊ธฐ๋ก(์ ์ ์์ฝ)๊ณผ ์ ๋ต ๊ฒฐํจ ๋ง์คํฌยท์์๋ยท์ ๋น ๋ฑ๊ธ์ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"from scipy import ndimage as ndi\n",
"\n",
"SZ = 64\n",
"CLASSES = [\"none\", \"crack\", \"corrosion\", \"damage\", \"leak\"]\n",
"KO_CLS = {\"none\": \"๊ฒฐํจ ์์\", \"crack\": \"๊ท ์ด\", \"corrosion\": \"๋ถ์\", \"damage\": \"ํ์\", \"leak\": \"๋์ \"}\n",
"CLS_W = {\"none\": 0.0, \"crack\": 1.4, \"corrosion\": 1.0, \"damage\": 1.6, \"leak\": 0.6} # ์์๋ ๊ณ์ฐ์ฉ ๊ฒฐํจ ์ ํ ๊ฐ์ค์น\n",
"GRADE_NAME = {1: \"ํ์ฅ์ ๋น\", 2: \"์ผ์ ์ ๋น\", 3: \"์ฐฝ์ ๋น\"}\n",
"YY, XX = np.mgrid[0:SZ, 0:SZ]\n",
"\n",
"\n",
"def grade_of(idx):\n",
" # [TODO : ํ์ ์ค์ต 6] ์ฅ๋น ๊ฒฐํจ ์ฌ์ง ์
๋ก๋ ๋ฐ ์์ ๋ถ์ ์๊ฐํ ํ์ ์ธํฐํ์ด์ค ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def blob(rng, cy, cx, r, rough=0.35):\n",
" \"\"\"๋ถ๊ท์นํ ์ํ ์์ญ ๋ง์คํฌ.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 6] ์ฅ๋น ๊ฒฐํจ ์ฌ์ง ์
๋ก๋ ๋ฐ ์์ ๋ถ์ ์๊ฐํ ํ์ ์ธํฐํ์ด์ค ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def draw(rng, cls, sev):\n",
" \"\"\"๊ฒฐํจ ์ ํยท์ฌ๊ฐ๋ sev(0~1)์ ๋ง๋ ์ ๋ต ๋ง์คํฌ์ ์ ์ ๋ณด๋ฅผ ๋ง๋ ๋ค.\"\"\"\n",
" m = np.zeros((SZ, SZ), bool)\n",
" if cls == \"crack\":\n",
" y, x, ang = rng.uniform(12, 52), rng.uniform(12, 52), rng.uniform(0, np.pi)\n",
" for _ in range(int(20 + 40 * sev)):\n",
" ang += rng.normal(0, 0.35); y += np.sin(ang) * 1.2; x += np.cos(ang) * 1.2\n",
" if 0 <= int(y) < SZ and 0 <= int(x) < SZ:\n",
" m[int(y), int(x)] = True\n",
" m = ndi.binary_dilation(m, iterations=1 + int(sev > 0.6))\n",
" elif cls == \"corrosion\":\n",
" for _ in range(int(rng.integers(1, 4))):\n",
" m |= blob(rng, rng.uniform(14, 50), rng.uniform(14, 50), 3 + 9 * sev * rng.uniform(0.6, 1.1))\n",
" elif cls == \"damage\":\n",
" m = blob(rng, rng.uniform(20, 44), rng.uniform(20, 44), 6 + 12 * sev, 0.3)\n",
" elif cls == \"leak\":\n",
" x0, y0 = rng.uniform(16, 48), rng.uniform(8, 24)\n",
" w, ln = 2 + 4 * sev, 12 + 26 * sev\n",
" m = (np.abs(XX - x0 - 0.15 * (YY - y0)) < w * (1 - 0.4 * (YY - y0) / ln)) & (YY > y0) & (YY < y0 + ln)\n",
" return m\n",
"\n",
"\n",
"def make_images(n=600, seed=0, mud=0.0, p_none=0.25, dark=False):\n",
" \"\"\"๊ฐ์ ์ธ๊ด ์ฌ์ง n์ฅ(+์งํ ์ค์ผ mud ๋น์จ)๊ณผ ๊ด๋ฆฌ ๊ธฐ๋กยท์ ๋ต. ๋ฐํ: meta(DataFrame), imgs(n,64,64,3), masks(n,64,64).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" imgs, masks, rows = np.zeros((n, SZ, SZ, 3), np.float32), np.zeros((n, SZ, SZ), bool), []\n",
" for i in range(n):\n",
" cls = \"none\" if rng.random() < p_none else str(rng.choice(CLASSES[1:], p=[0.25, 0.3, 0.25, 0.2]))\n",
" sev = 0.0 if cls == \"none\" else float(rng.beta(1.6, 2.0))\n",
" base = np.array([0.42, 0.46, 0.40]) * (1 + 0.5 * ndi.gaussian_filter(rng.normal(size=(SZ, SZ)), 8)[..., None] * 3)\n",
" img = base + rng.normal(0, 0.012, (SZ, SZ, 3))\n",
" m = draw(rng, cls, sev)\n",
" a = ndi.gaussian_filter(m.astype(float), 0.8)[..., None] # ๊ฒฝ๊ณ๋ฅผ ๋ถ๋๋ฝ๊ฒ ์๋๋ค\n",
" if cls == \"crack\":\n",
" col = img * rng.uniform(0.2, 0.45)\n",
" elif cls == \"corrosion\":\n",
" col = np.array([0.55, 0.30, 0.12]) * rng.uniform(0.75, 1.15) * (0.85 + 0.3 * rng.random((SZ, SZ, 1)))\n",
" elif cls == \"damage\":\n",
" col = np.ones_like(img) * np.array([0.34, 0.34, 0.36]) * (1.0 - 0.45 * sev) * rng.uniform(0.75, 1.0)\n",
" rim = m & ~ndi.binary_erosion(m, iterations=2)\n",
" a = np.maximum(a, 0.0); img = np.where(rim[..., None], 0.7, img)\n",
" elif cls == \"leak\":\n",
" col = np.ones_like(img) * np.array([0.10, 0.09, 0.08]) * rng.uniform(0.8, 2.6)\n",
" else:\n",
" col = img\n",
" img = img * (1 - a) + col * a\n",
" if rng.random() < 0.35: # ๋ฌดํดํ ์ผ๋ฃฉ(๊ฒฐํจ ์๋): ์
์ ๊ฐ์ ๋ฒ์ง\n",
" st = blob(rng, rng.uniform(5, 58), rng.uniform(5, 58), rng.uniform(6, 14), 0.5)\n",
" al = ndi.gaussian_filter(st.astype(float), 2.5)[..., None] * rng.uniform(0.15, 0.4)\n",
" img = img * (1 - al) + np.array([0.50, 0.40, 0.30]) * al\n",
" if rng.random() < 0.3: # ๋ฌดํดํ ๊ธด ์คํฌ๋์น(๋ฐ์ ์ )\n",
" y0, x0, ang = rng.uniform(5, 58), rng.uniform(5, 58), rng.uniform(0, np.pi)\n",
" for t in np.linspace(0, 25, 40):\n",
" yy, xx = int(y0 + t * np.sin(ang)), int(x0 + t * np.cos(ang))\n",
" if 0 <= yy < SZ and 0 <= xx < SZ:\n",
" img[yy, xx] = np.clip(img[yy, xx] * 1.5, 0, 1)\n",
" if mud > 0: # ์งํ ์ค์ผ: ๊ฒฐํจ ์๋ฅผ ๋ฎ์ ์ ์๋ค(์ ๋ต ๋ง์คํฌ๋ ๊ทธ๋๋ก)\n",
" mm = np.zeros((SZ, SZ), bool)\n",
" while mm.mean() < mud:\n",
" mm |= blob(rng, rng.uniform(0, SZ), rng.uniform(0, SZ), rng.uniform(5, 11), 0.5)\n",
" img[mm] = np.array([0.38, 0.30, 0.22]) * (0.85 + 0.3 * rng.random((mm.sum(), 1)))\n",
" gain = rng.uniform(0.3, 0.55) if dark else rng.uniform(0.5, 1.15)\n",
" ang = rng.uniform(0, 2 * np.pi)\n",
" light = gain * (1 + 0.3 * (np.cos(ang) * (XX - 32) + np.sin(ang) * (YY - 32)) / 32)\n",
" imgs[i] = np.clip(img * light[..., None] + rng.normal(0, 0.025, (SZ, SZ, 3)), 0, 1)\n",
" masks[i] = m\n",
" area = float(m.mean())\n",
" depth = {\"none\": 0.0, \"crack\": 0.3 + 3.0 * sev, \"corrosion\": 0.1 + 1.5 * sev, \"damage\": 1.0 + 8.0 * sev, \"leak\": 0.0}[cls] * rng.lognormal(0, 0.1)\n",
" idx = float(np.clip(100 * (1 - np.exp(-(CLS_W[cls] * area * 9 + 0.05 * depth))) + rng.normal(0, 1.5), 0, 100))\n",
" rows.append({\"img_id\": f\"IMG{seed:02d}{i:04d}\", \"cls\": cls, \"sev\": sev, \"area_frac\": area, \"depth_mm\": depth, \"damage_idx\": idx,\n",
" \"grade\": int(grade_of(idx)), \"gain\": gain, \"mud\": mud})\n",
" meta = pd.DataFrame(rows)\n",
" # ์ฅ๋น ๊ด๋ฆฌ ๊ธฐ๋ก(์ ์ ์์ฝ): ์ฌ์ง ํ์ ๊ณผ ๋์กฐํ ๊ฐ\n",
" age = rng.integers(1, 25, n)\n",
" meta[\"day\"] = np.sort(rng.integers(0, 90, n))\n",
" meta[\"equip_type\"] = rng.choice([\"vehicle\", \"generator\", \"comm\", \"trailer\"], n, p=[0.4, 0.2, 0.2, 0.2])\n",
" meta[\"mfg_year\"] = 2026 - age\n",
" meta[\"op_hours\"] = np.clip(rng.normal(180 * age, 400, n) + 8 * meta[\"damage_idx\"], 10, 20000)\n",
" meta[\"env\"] = rng.choice([\"coastal\", \"inland\"], n, p=[0.3, 0.7])\n",
" reg = np.clip(meta[\"grade\"] + rng.choice([-1, 0, 0, 0, 0, 0, 1, 2], n), 1, 3) # ์ฅ๋น ๊ด๋ฆฌ ๊ธฐ๋ก์ ์ง์ ์ ๋น ๋ฑ๊ธ\n",
" meta[\"reg_grade\"] = reg\n",
" meta[\"emergency\"] = rng.random(n) < 0.04 # ๊ธด๊ธ ์ ๊ฒ ์์ฒญ(ํ์ง ๋ถ๋์ด์ด๋ ๋ณด์กด)\n",
" return meta, imgs, masks\n",
"# ---- ์ ์ฒ๋ฆฌ(์กฐ๋ช
๋ณด์ ยทํ์คํ)์ ๊ฒฐํจ ํ๋ณด ๋ถํ , ํน์ง ์ถ์ถ (cv2 ๋์ numpyยทscipy ์ฌ์ฉ) ----\n",
"def correct(img):\n",
" \"\"\"์กฐ๋ช
๋ณด์ : ๋ฐ๊ธฐ์ ํ๋ฉด(์กฐ๋ช
๊ธฐ์ธ๊ธฐ)์ ์ ํฉํด ๋๋๋ค. ์ด๋์ด ๊ฒฐํจ ํ์๋ ์ ์ธํ๊ณ ๋ ๋ฒ ๋ฐ๋ณต ์ ํฉํ๋ค. ๋ฐํ: ๋ณด์ ๋ฐ๊ธฐ(์ ์ ๋ฉด ์ฝ 1), ์ ๋น์จ.\"\"\"\n",
" g = img.mean(axis=2)\n",
" A = np.c_[np.ones(SZ * SZ), XX.ravel() / SZ, YY.ravel() / SZ]\n",
" keep = np.ones(SZ * SZ, bool)\n",
" for _ in range(3):\n",
" coef, *_ = np.linalg.lstsq(A[keep], g.ravel()[keep], rcond=None)\n",
" plane = (A @ coef).reshape(SZ, SZ)\n",
" keep = (np.abs(g - plane) < 0.08 * np.maximum(plane, 1e-3)).ravel()\n",
" if keep.sum() < 200:\n",
" keep[:] = True\n",
" c = g / np.maximum(plane, 1e-3)\n",
" chroma = img[..., 0] / (img.sum(axis=2) + 1e-6)\n",
" return c, chroma\n",
"\n",
"\n",
"SEGP = {\"dark\": 0.7, \"rust\": 0.5, \"rim\": 1.45} # ๋ถํ ์๊ณ๊ฐ(์ด๋์ ๋ณด์ ๋ฐ๊ธฐ, ๋ถ์ ๋น์จ, ๋ฐ์ ํ
๋๋ฆฌ): ์ฌ๋ณด์ ๋์\n",
"\n",
"\n",
"def segment(img):\n",
" \"\"\"๊ฒฐํจ ํ๋ณด ๋ง์คํฌ: ์ด๋์ด ์์ญ(๊ท ์ดยท๋์ ยทํ์) + ๋ถ์ ๊ฐ์ ์์ญ(๋ถ์) + ๋ฐ์ ํ
๋๋ฆฌ(ํ์).\"\"\"\n",
" c, chroma = correct(img)\n",
" dark = c < SEGP[\"dark\"]\n",
" rust = (chroma > SEGP[\"rust\"]) & (c > 0.45)\n",
" rim = c > SEGP[\"rim\"]\n",
" m = dark | rust | ndi.binary_dilation(rim, iterations=1) & ndi.binary_dilation(dark, iterations=3)\n",
" m = ndi.binary_opening(m, structure=np.ones((2, 2))) if m.sum() > 12 else m\n",
" return m, c, chroma, dark, rust, rim\n",
"\n",
"\n",
"def extract(img):\n",
" \"\"\"๋ถํ ๊ฒฐ๊ณผ์ ์ยท๋ชจ์์์ 24๊ฐ ํน์ง์ ๋ง๋ ๋ค.\"\"\"\n",
" m, c, chroma, dark, rust, rim = segment(img)\n",
" lab, n = ndi.label(m)\n",
" area = float(m.sum())\n",
" if n:\n",
" sizes = ndi.sum(m, lab, range(1, n + 1))\n",
" k = int(np.argmax(sizes)) + 1\n",
" sl = ndi.find_objects(lab)[k - 1]\n",
" h, w = sl[0].stop - sl[0].start, sl[1].stop - sl[1].start\n",
" big = float(sizes.max())\n",
" elong, fill = max(h, w) / max(min(h, w), 1), big / (h * w)\n",
" else:\n",
" big, elong, fill = 0.0, 1.0, 0.0\n",
" gy, gx = np.gradient(c)\n",
" mag = np.hypot(gx, gy)\n",
" inside = m.sum() > 0\n",
" return {\"area_px\": area, \"area_frac\": area / SZ ** 2, \"n_comp\": float(n), \"big_px\": big, \"elong\": elong, \"fill\": fill,\n",
" \"dark_frac\": float(dark.mean()), \"rust_frac\": float(rust.mean()), \"rim_frac\": float(rim.mean()),\n",
" \"c_mean\": float(c.mean()), \"c_std\": float(c.std()), \"c_min\": float(c.min()), \"edge_mean\": float(mag.mean()), \"edge_p95\": float(np.quantile(mag, 0.95)),\n",
" \"chroma_mean\": float(chroma.mean()), \"chroma_max\": float(chroma.max()),\n",
" \"in_dark\": float(c[m].mean()) if inside else 1.0, \"in_chroma\": float(chroma[m].mean()) if inside else float(chroma.mean()),\n",
" \"ring_contrast\": float(c[ndi.binary_dilation(m, iterations=3) & ~m].mean() - c[m].mean()) if inside and (~m).any() else 0.0,\n",
" \"thin\": float(1 - fill), \"gain\": float(img.mean()), \"sat\": float((img.max(axis=2) - img.min(axis=2)).mean()),\n",
" \"cy\": float(ndi.center_of_mass(m)[0] / SZ) if inside else 0.5, \"cx\": float(ndi.center_of_mass(m)[1] / SZ) if inside else 0.5}\n",
"\n",
"\n",
"def feature_table(imgs):\n",
" return pd.DataFrame([extract(im) for im in imgs])\n",
"# ---- ํ์ ํ์ดํ๋ผ์ธ: ๊ฒฐํจ ์ ํ ๋ถ๋ฅ + ์์๋ ํ๊ท(๋ถ๋ฅ ํ๋ฅ ํฌํจ) -> ์ ๋น ๋ฑ๊ธ ----\n",
"def fit_pipeline(meta, F):\n",
" \"\"\"๊ฒฐํจ ์ ํ ๋ถ๋ฅ๊ธฐ์ ์์๋ ํ๊ท๊ธฐ๋ฅผ ํ์ตํ๋ค. ํ๊ท๊ธฐ ์
๋ ฅ์ ๋ถ๋ฅ ํ๋ฅ ์ ๊ต์ฐจ ์์ธก์ผ๋ก ๋ง๋ค์ด ๋์ค์ ๋ง๋๋ค.\"\"\"\n",
" clf = RandomForestClassifier(n_estimators=150, random_state=0, n_jobs=1)\n",
" P = cross_val_predict(clf, F, meta[\"cls\"], cv=4, method=\"predict_proba\")\n",
" clf.fit(F, meta[\"cls\"])\n",
" reg = GradientBoostingRegressor(n_estimators=200, max_depth=3, learning_rate=0.06, random_state=0).fit(np.c_[F.values, P], meta[\"damage_idx\"])\n",
" return {\"clf\": clf, \"reg\": reg, \"cols\": list(F.columns)}\n",
"\n",
"\n",
"def predict_pipeline(pl, F):\n",
" X = F[pl[\"cols\"]]\n",
" P = pl[\"clf\"].predict_proba(X)\n",
" idx = np.clip(pl[\"reg\"].predict(np.c_[X.values, P]), 0, 100)\n",
" return pl[\"clf\"].classes_[P.argmax(axis=1)], P, idx, P.max(axis=1)\n",
"# ---- ์ฌ์ง ํ์ง ๊ฒ์ดํธ: ์งํยท์ค์ผ ๊ฐ๋ฆผ ์ถ์ ๊ณผ ํ์ ์ ๋ขฐ๋ ์ง์ ----\n",
"def mud_est(img):\n",
" \"\"\"์งํ(๊ฐ์, ์ฑ๋ ๋ฎ์ ๋ถ์ ๊ธฐ) ํ์ ๋น์จ์ ์ถ์ ํ๋ค. 3x3 ํ๊ท ์ผ๋ก ์ก์์ ์ค์ธ ๋ค ์ ๋น์จ๋ก ํ๋จํ๋ฉฐ, ์ค์ ๊ฐ๋ฆผ ๋น์จ๋ณด๋ค ์๊ฒ ๋์ค์ง๋ง ์์๋ ์ ์ง๋๋ค.\"\"\"\n",
" sm = ndi.uniform_filter(img, size=(3, 3, 1))\n",
" s = sm.sum(axis=2) + 1e-6\n",
" rr, gg = sm[..., 0] / s, sm[..., 1] / s\n",
" return float(((rr > 0.385) & (rr < 0.46) & (gg < 0.325)).mean())\n",
"\n",
"\n",
"def confidence(P, idx, F, mud):\n",
" \"\"\"ํ์ ์ ๋ขฐ๋ ์ง์ 0~100 = 50 x ๋ถ๋ฅ ํ๋ฅ ์ต๋๊ฐ + 35 x ๋ฑ๊ธ ๊ฒฝ๊ณ๊น์ง ๊ฑฐ๋ฆฌ(8์ ์์ ํฌํ) + 15 x ์ฌ์ง ํ์ง. ์ค์ผ ์์ฌ(mud ์ถ์ 1% ์ด์)์ด๋ฉด ์ํ 75๋ก ์ ํํ๋ค.\"\"\"\n",
" bd = np.minimum(np.abs(idx - 35), np.abs(idx - 65))\n",
" qual = np.clip(1.0 - np.clip(0.55 - F[\"gain\"].values, 0, None) * 2 - np.clip(0.9 - F[\"c_mean\"].values, 0, None), 0, 1)\n",
" c = 50 * P.max(axis=1) + 35 * np.clip(bd / 8, 0, 1) + 15 * qual\n",
" return np.where(np.asarray(mud) >= 0.01, np.minimum(c, 75.0), c)\n",
"\n",
"mtr, itr, _ = make_images(600, seed=1)\n",
"Ftr = feature_table(itr)\n",
"pl = fit_pipeline(mtr, Ftr)\n",
"reg_p = GradientBoostingRegressor(n_estimators=150, max_depth=3, learning_rate=0.07, random_state=0)\n",
"Xd = np.c_[Ftr[pl[\"cols\"]].values, cross_val_predict(RandomForestClassifier(n_estimators=80, random_state=0, n_jobs=1), Ftr, mtr[\"cls\"], cv=3, method=\"predict_proba\")]\n",
"dm = mtr[\"depth_mm\"].values > 0.2\n",
"depth_model = reg_p.fit(Xd[dm], mtr[\"depth_mm\"].values[dm])\n",
"MM2 = 1.5 ** 2 # 1ํ์ = 1.5mm ๊ฐ์ -> ๋ฉด์ mm2\n",
"GROUPS = {\"๊ฒฐํจ ๋ฉด์ ยท๋ชจ์\": [\"area_px\", \"area_frac\", \"n_comp\", \"big_px\", \"elong\", \"fill\", \"thin\", \"cy\", \"cx\"], \"๋ช
์ ๋๋น\": [\"dark_frac\", \"c_mean\", \"c_std\", \"c_min\", \"in_dark\", \"ring_contrast\"],\n",
" \"๋ถ์ ๊ฐ์ ์์\": [\"rust_frac\", \"chroma_mean\", \"chroma_max\", \"in_chroma\", \"sat\"], \"๊ฐ์ฅ์๋ฆฌยทํ
๋๋ฆฌ\": [\"edge_mean\", \"edge_p95\", \"rim_frac\"], \"์กฐ๋ช
๋ฐ๊ธฐ\": [\"gain\"]}\n",
"MEAN = Ftr.mean()\n",
"\n",
"\n",
"def contrib(F):\n",
" \"\"\"๋ณ์๊ตฐ์ ํ๊ท ๊ฐ์ผ๋ก ๋์ฒดํ์ ๋ ์์๋ ์ ์ ๋ณํ(์์ = ๊ทธ ๋ณ์๊ตฐ์ด ์์๋๋ฅผ ๋์ธ ์์ธ). ์ฌ์ง ์ฌ๋ฌ ์ฅ์ ํ ๋ฒ์ ๊ณ์ฐํ๋ค.\"\"\"\n",
" full = predict_pipeline(pl, F)[2]\n",
" out = {}\n",
" for g, cols in GROUPS.items():\n",
" x = F.copy(); x[cols] = MEAN[cols].values\n",
" out[g] = full - predict_pipeline(pl, x)[2]\n",
" return pd.DataFrame(out, index=F.index)\n",
"\n",
"\n",
"def top3(F):\n",
" return contrib(F).apply(lambda r: r.reindex(r.abs().sort_values(ascending=False).index)[:3].to_dict(), axis=1)\n",
"\n",
"\n",
"def inspect(img, pid):\n",
" \"\"\"์
๋ก๋ ์ฌ์ง ํ ์ฅ์ ํ์ ํ๋ค. ๊ฒฐ๊ณผ๋ ํญ์ '๊ฒ์ฌ ๋๊ธฐ'์ด๋ฉฐ ์๋ ํ์ ํ์ง ์๋๋ค.\"\"\"\n",
" F = feature_table([img])\n",
" cls, P, idx, _ = predict_pipeline(pl, F)\n",
" mud = mud_est(img)\n",
" conf = float(confidence(P, idx, F, [mud])[0])\n",
" m = segment(img)[0]\n",
" lab, n = ndi.label(m)\n",
" box = None\n",
" if n:\n",
" k = int(np.argmax(ndi.sum(m, lab, range(1, n + 1)))) + 1\n",
" sl = ndi.find_objects(lab)[k - 1]\n",
" box = (sl[1].start, sl[0].start, sl[1].stop - sl[1].start, sl[0].stop - sl[0].start)\n",
" dep = float(np.clip(depth_model.predict(np.c_[F[pl[\"cols\"]].values, P])[0], 0, None)) if cls[0] in (\"crack\", \"corrosion\", \"damage\") else 0.0\n",
" if mud >= 0.03 or F[\"gain\"].iloc[0] < 0.17:\n",
" state = \"์ฌ์ดฌ์ ์๊ตฌ(์ค์ผยท์ ์กฐ๋)\"\n",
" elif conf >= 80:\n",
" state = \"๊ฒ์ฌ ๋๊ธฐ(์ ๋ขฐ๋ ๋์, ๊ฒ์ฌ๊ด ํ์ธ ํ ํ์ )\"\n",
" else:\n",
" state = \"๊ฒ์ฌ ๋๊ธฐ(์ ๋ขฐ๋ ๋ฎ์, ๊ฒ์ฌ๊ด ์ ๋ฐ ํ์ธ)\"\n",
" return {\"id\": pid, \"cls\": cls[0], \"area_mm2\": float(F[\"area_px\"].iloc[0] * MM2), \"depth\": dep, \"idx\": float(idx[0]), \"grade\": int(grade_of(idx[0])), \"conf\": conf, \"mud\": mud, \"box\": box, \"mask\": m, \"state\": state, \"F\": F}\n",
"\n",
"\n",
"# ---------------- 1) ์
๋ก๋ ์๋ฎฌ๋ ์ด์
: ์กฐ๊ฑด์ด ๋ค๋ฅธ ์ฌ์ง 8์ฅ ----------------\n",
"mt, it, kt = make_images(300, seed=2)\n",
"mud_m, imud, kmud = make_images(30, seed=11, mud=0.2)\n",
"dark_m, idk, kdk = make_images(30, seed=12, dark=True)\n",
"picks = [(\"U01\", it[next(i for i in range(300) if mt.cls[i] == \"crack\")], kt, mt, \"crack\"), (\"U02\", it[next(i for i in range(300) if mt.cls[i] == \"corrosion\")], kt, mt, \"corrosion\"),\n",
" (\"U03\", it[next(i for i in range(300) if mt.cls[i] == \"damage\")], kt, mt, \"damage\"), (\"U04\", it[next(i for i in range(300) if mt.cls[i] == \"leak\")], kt, mt, \"leak\"),\n",
" (\"U05\", it[next(i for i in range(300) if mt.cls[i] == \"none\")], kt, mt, \"none\"), (\"U06\", idk[0], kdk, dark_m, None), (\"U07\", imud[0], kmud, mud_m, None), (\"U08\", imud[1], kmud, mud_m, None)]\n",
"res = []\n",
"for pid, img, _, _, _ in picks:\n",
" res.append(inspect(img, pid))\n",
"true_rows = [mt.loc[next(i for i in range(300) if mt.cls[i] == c)] for c in (\"crack\", \"corrosion\", \"damage\", \"leak\", \"none\")] + [dark_m.loc[0], mud_m.loc[0], mud_m.loc[1]]\n",
"tb = pd.DataFrame([{\"์ฌ์ง\": r[\"id\"], \"ํ์ ์ ํ\": KO_CLS[r[\"cls\"]], \"์ค์ ์ ํ\": KO_CLS[t[\"cls\"]], \"๋ฉด์ (mm2)\": r[\"area_mm2\"], \"๊น์ด(mm)\": r[\"depth\"], \"์์๋\": r[\"idx\"], \"์ค์ ์์๋\": t[\"damage_idx\"],\n",
" \"๋ฑ๊ธ\": GRADE_NAME[r[\"grade\"]], \"์ค์ ๋ฑ๊ธ\": GRADE_NAME[int(t[\"grade\"])], \"์ ๋ขฐ๋\": r[\"conf\"], \"์ค์ผ\": r[\"mud\"]} for r, t in zip(res, true_rows)])\n",
"print(\"[1] ์
๋ก๋ ํ์ ๊ฒฐ๊ณผ(8์ฅ, ๋ฉด์ ์ 1ํ์ 1.5mm ๊ฐ์ ) [๊ต์ก์ฉ๊ฐ์์๋ฃ]\")\n",
"print(tb.round(2).to_string(index=False))\n",
"for r in res:\n",
" print(f\" {r['id']}: {r['state']}\")\n",
"\n",
"# ---------------- 2) ๊ธฐ์ฌ๋ ์์ 3๊ฐ ์๋ ์ฐ์ถ(ํ๊ฐ 150์ฅ) ----------------\n",
"mte, ite, _ = make_images(300, seed=2)\n",
"Fte = feature_table(ite)\n",
"T3 = top3(Fte)\n",
"ok = int(sum(len(t) == 3 and all(np.isfinite(v) for v in t.values()) for t in T3))\n",
"texts = list(T3)\n",
"print(f\"\\n[2] ํ๊ฐ {len(ite)}์ฅ ์ค ๊ธฐ์ฌ๋ ์์ 3๊ฐ ์์ธ ์๋ ์ฐ์ถ {ok}๊ฑด ({ok / len(ite):.0%}, ๋ชฉํ 100%)\")\n",
"avg = pd.DataFrame(texts).abs().mean().sort_values(ascending=False)\n",
"print(\" ๋ณ์๊ตฐ๋ณ ํ๊ท ๊ธฐ์ฌ ํฌ๊ธฐ(์์๋ ์ ):\", \", \".join(f\"{k} {v:.1f}\" for k, v in avg.items()))\n",
"\n",
"\n",
"def explain(r):\n",
" t3 = top3(r[\"F\"]).iloc[0]\n",
" why = {\"๊ฒฐํจ ๋ฉด์ ยท๋ชจ์\": (\"๊ฒฐํจ ๋ฉด์ ยท๋ชจ์์ด ํ๊ท ๋ณด๋ค ์ปค์\", \"๊ฒฐํจ ๋ฉด์ ยท๋ชจ์์ด ํ๊ท ๋ณด๋ค ์์\"), \"๋ช
์ ๋๋น\": (\"ํ๋ฉด๊ณผ ํฌ๊ฒ ๋ค๋ฅธ ์ด๋์ด ์์ญ์ด ์์ด\", \"์ด๋์ด ์์ญ์ด ์ ์ด\"),\n",
" \"๋ถ์ ๊ฐ์ ์์\": (\"๋ถ์ ๊ฐ์ ๋ถ์ ์์์ด ๋์ด\", \"๋ถ์ ๊ฐ์ ์์์ด ์ ์ด\"), \"๊ฐ์ฅ์๋ฆฌยทํ
๋๋ฆฌ\": (\"๋๋ ทํ ํ
๋๋ฆฌ๊ฐ ์์ด\", \"ํ
๋๋ฆฌ ๋จ์๊ฐ ์ฝํด\"), \"์กฐ๋ช
๋ฐ๊ธฐ\": (\"์กฐ๋ช
๋ฐ๊ธฐ ์ํฅ์ผ๋ก\", \"์กฐ๋ช
๋ฐ๊ธฐ ์ํฅ์ผ๋ก\")}\n",
" main = list(t3)[0]\n",
" return f\"{r['id']}: {why[main][t3[main] < 0]} ์์๋ {r['idx']:.0f}์ ({GRADE_NAME[r['grade']]}) ํ์ . ์ฃผ์ ์์ธ \" + \", \".join(f\"{g}({v:+.1f}์ )\" for g, v in t3.items())\n",
"\n",
"\n",
"print(\"[3] ํ์ ๊ทผ๊ฑฐ ์์ฐ์ด ์ค๋ช
(๊ฒฐํจ ์๋ 3์ฅ)\")\n",
"for r in (res[0], res[1], res[2]):\n",
" print(\" -\", explain(r))\n",
"\n",
"# ---------------- 3) ์์ ๋ถ์ ์๊ฐํ ์ ํ๋: ๋ฐ์ค์ ์ ๋ต ๋ง์คํฌ ๊ฒน์นจ ----------------\n",
"ious = []\n",
"for r, (pid, img, kk, mm, _) in zip(res[:4], picks[:4]):\n",
" i = next(j for j in range(300) if mt.cls[j] == {\"U01\": \"crack\", \"U02\": \"corrosion\", \"U03\": \"damage\", \"U04\": \"leak\"}[pid])\n",
" gt = kk[i]\n",
" ious.append((pid, (r[\"mask\"] & gt).sum() / max((r[\"mask\"] | gt).sum(), 1)))\n",
"print(\"[4] ์์ ๋ถ์ ํ์์ ์ ๋ต ๋ง์คํฌ์ IoU:\", \", \".join(f\"{p} {v:.2f}\" for p, v in ious))\n",
"print(\" ํ๋ฉด์ ์๋ ํ์ ๋ฒํผ ์์ด '๊ฒ์ฌ ๋๊ธฐ ๋ฑ๋ก'๋ง ์ ๊ณตํ๋ฉฐ, ์ต์ข
์ ๋น ๋ฑ๊ธ์ ๊ฒ์ฌ๊ด์ด ํ์ ํ๋ค(M7).\")\n",
"\n",
"fig, ax = plt.subplots(2, 4, figsize=(14, 7))\n",
"for a, (pid, img, _, _, _), r in zip(ax.ravel(), picks, res):\n",
" a.imshow(img)\n",
" a.contour(r[\"mask\"], levels=[0.5], colors=\"yellow\", linewidths=1)\n",
" if r[\"box\"]:\n",
" x, y, w, h = r[\"box\"]; a.add_patch(plt.Rectangle((x - .5, y - .5), w, h, fill=False, ec=\"red\", lw=1.5))\n",
" a.set_title(f\"{pid} {r['cls']} idx {r['idx']:.0f} grade {r['grade']} conf {r['conf']:.0f}\", fontsize=9); a.axis(\"off\")\n",
"plt.suptitle(\"Upload inspection view (synthetic photos): yellow = defect region, red = box\"); plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
06 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[1] ์
๋ก๋ ํ์ ๊ฒฐ๊ณผ(8์ฅ, ๋ฉด์ ์ 1ํ์ 1.5mm ๊ฐ์ ) [๊ต์ก์ฉ๊ฐ์์๋ฃ]\n",
" ์ฌ์ง ํ์ ์ ํ ์ค์ ์ ํ ๋ฉด์ (mm2) ๊น์ด(mm) ์์๋ ์ค์ ์์๋ ๋ฑ๊ธ ์ค์ ๋ฑ๊ธ ์ ๋ขฐ๋ ์ค์ผ\n",
"U01 ๊ท ์ด ๊ท ์ด 121.50 1.67 19.39 25.53 ํ์ฅ์ ๋น ํ์ฅ์ ๋น 91.72 0.00\n",
"U02 ๋ถ์ ๋ถ์ 171.00 0.44 24.12 24.49 ํ์ฅ์ ๋น ํ์ฅ์ ๋น 93.58 0.00\n",
"U03 ํ์ ํ์ 857.25 4.91 82.32 85.72 ์ฐฝ์ ๋น ์ฐฝ์ ๋น 96.30 0.00\n",
"U04 ๋์ ๋์ 265.50 0.00 14.31 14.60 ํ์ฅ์ ๋น ํ์ฅ์ ๋น 88.68 0.00\n",
"U05 ๊ฒฐํจ ์์ ๊ฒฐํจ ์์ 0.00 0.00 1.17 0.00 ํ์ฅ์ ๋น ํ์ฅ์ ๋น 91.61 0.00\n",
"U06 ๋์ ๋์ 231.75 0.00 15.84 10.12 ํ์ฅ์ ๋น ํ์ฅ์ ๋น 86.90 0.00\n",
"U07 ํ์ ๊ฒฐํจ ์์ 103.50 3.21 70.75 1.96 ์ฐฝ์ ๋น ํ์ฅ์ ๋น 64.69 0.02\n",
"U08 ํ์ ํ์ 339.75 4.17 57.16 66.53 ์ผ์ ์ ๋น ์ฐฝ์ ๋น 72.90 0.02\n",
" U01: ๊ฒ์ฌ ๋๊ธฐ(์ ๋ขฐ๋ ๋์, ๊ฒ์ฌ๊ด ํ์ธ ํ ํ์ )\n",
" U02: ๊ฒ์ฌ ๋๊ธฐ(์ ๋ขฐ๋ ๋์, ๊ฒ์ฌ๊ด ํ์ธ ํ ํ์ )\n",
" U03: ๊ฒ์ฌ ๋๊ธฐ(์ ๋ขฐ๋ ๋์, ๊ฒ์ฌ๊ด ํ์ธ ํ ํ์ )\n",
" U04: ๊ฒ์ฌ ๋๊ธฐ(์ ๋ขฐ๋ ๋์, ๊ฒ์ฌ๊ด ํ์ธ ํ ํ์ )\n",
" U05: ๊ฒ์ฌ ๋๊ธฐ(์ ๋ขฐ๋ ๋์, ๊ฒ์ฌ๊ด ํ์ธ ํ ํ์ )\n",
" U06: ์ฌ์ดฌ์ ์๊ตฌ(์ค์ผยท์ ์กฐ๋)\n",
" U07: ๊ฒ์ฌ ๋๊ธฐ(์ ๋ขฐ๋ ๋ฎ์, ๊ฒ์ฌ๊ด ์ ๋ฐ ํ์ธ)\n",
" U08: ๊ฒ์ฌ ๋๊ธฐ(์ ๋ขฐ๋ ๋ฎ์, ๊ฒ์ฌ๊ด ์ ๋ฐ ํ์ธ)\n",
"\n",
"[2] ํ๊ฐ 300์ฅ ์ค ๊ธฐ์ฌ๋ ์์ 3๊ฐ ์์ธ ์๋ ์ฐ์ถ 300๊ฑด (100%, ๋ชฉํ 100%)\n",
" ๋ณ์๊ตฐ๋ณ ํ๊ท ๊ธฐ์ฌ ํฌ๊ธฐ(์์๋ ์ ): ๊ฒฐํจ ๋ฉด์ ยท๋ชจ์ 25.1, ๋ถ์ ๊ฐ์ ์์ 6.3, ๋ช
์ ๋๋น 5.3, ์กฐ๋ช
๋ฐ๊ธฐ 2.6, ๊ฐ์ฅ์๋ฆฌยทํ
๋๋ฆฌ 1.0\n",
"[3] ํ์ ๊ทผ๊ฑฐ ์์ฐ์ด ์ค๋ช
(๊ฒฐํจ ์๋ 3์ฅ)\n",
" - U01: ๊ฒฐํจ ๋ฉด์ ยท๋ชจ์์ด ํ๊ท ๋ณด๋ค ์์ ์์๋ 19์ (ํ์ฅ์ ๋น) ํ์ . ์ฃผ์ ์์ธ ๊ฒฐํจ ๋ฉด์ ยท๋ชจ์(-18.2์ ), ๋ช
์ ๋๋น(-5.0์ ), ๋ถ์ ๊ฐ์ ์์(-1.0์ )\n",
" - U02: ๊ฒฐํจ ๋ฉด์ ยท๋ชจ์์ด ํ๊ท ๋ณด๋ค ์์ ์์๋ 24์ (ํ์ฅ์ ๋น) ํ์ . ์ฃผ์ ์์ธ ๊ฒฐํจ ๋ฉด์ ยท๋ชจ์(-11.6์ ), ๋ช
์ ๋๋น(-1.8์ ), ๋ถ์ ๊ฐ์ ์์(+1.7์ )\n",
" - U03: ๊ฒฐํจ ๋ฉด์ ยท๋ชจ์์ด ํ๊ท ๋ณด๋ค ์ปค์ ์์๋ 82์ (์ฐฝ์ ๋น) ํ์ . ์ฃผ์ ์์ธ ๊ฒฐํจ ๋ฉด์ ยท๋ชจ์(+23.9์ ), ๋ถ์ ๊ฐ์ ์์(+19.6์ ), ๋ช
์ ๋๋น(+4.7์ )\n",
"[4] ์์ ๋ถ์ ํ์์ ์ ๋ต ๋ง์คํฌ์ IoU: U01 0.95, U02 0.64, U03 0.80, U04 0.98\n",
" ํ๋ฉด์ ์๋ ํ์ ๋ฒํผ ์์ด '๊ฒ์ฌ ๋๊ธฐ ๋ฑ๋ก'๋ง ์ ๊ณตํ๋ฉฐ, ์ต์ข
์ ๋น ๋ฑ๊ธ์ ๊ฒ์ฌ๊ด์ด ํ์ ํ๋ค(M7).\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
07. ์ธ๊ด ํ์ ๊ฒฐ๊ณผ์ ๋ํ ๊ฒ์ฌ๊ด ์๋ ๋ณด์ ๋ฐ ์ต์ข
์ ๋น ๋ฑ๊ธ HITL ์น์ธ ์ ์ฐจ ์ค๊ณ\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ธ๊ด ํ์ ๊ฒฐ๊ณผ์ ๋ํ ๊ฒ์ฌ๊ด ์๋ ๋ณด์ ๋ฐ HITL ์น์ธ ์ ์ฐจ๋ฅผ ์ค๊ณํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **AI ์ธ๊ด ํ์ ์ ๋ํ ๊ฒ์ฌ๊ด ์๋ ๋ณด์ ๊ณผ ์ต์ข
์ ๋น ๋ฑ๊ธ ์น์ธ HITL ์ ์ฐจ๋ฅผ ์ค๊ณํ๊ณ ๊ฒ์ฌ ์ด์ ๋น์ฉ์ ์ต์ ํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ ๋ขฐ๋ ๊ธฐ์ค์ผ๋ก ๊ฐ์ด ์น์ธ๊ณผ ์ ๋ฐ ํ๋
์ ๋๋๋ ์ด์์์ ์ ์ ์ ๋ฐ ํ๋
๊ดํ๊ณผ ๋น๊ตํด ๋น์ฉ(ํ๋
ยท๊ณผ์ยท๊ณผ๋ ํ์ ์์ค)์ ๊ณ์ฐํ๊ณ , ์ ๋ขฐ๋ ๊ธฐ์ค๊ณผ ๊ฒ์ฌ๊ด ์ผ์ผ ํ๋์ ๋ฐ๋ฅธ ๋ฏผ๊ฐ๋๋ฅผ ๋ณธ๋ค ๊ณผ์ ์ ์งํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/optimization/qlt03_simulator.py` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ํ์ค์ ๊ตฐ์ ์ ์ฝ์กฐ๊ฑด ๋ฐ์๋ ๋ฐ ์๋๋ฆฌ์ค๋ณ ๋์ ์ ์ ์ ์ ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ์ต์ ํ ์ ์ฉ ์ ๋น์ฉ 15% ์ ๊ฐ ๋๋ ์ฌ๊ณ ํ์ ์จ 20% ํฅ์\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์ ํ/์ ์ ๊ณํ๋ฒ(LP/MIP) ์ต์ ํ ํด ๋์ถ ๋ฐ ์๋ฎฌ๋ ์ด์
์๋ฃ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ๊ณผ๊ฑฐ ์ค์ ๋ฐ์ดํฐ ๋๋น ์ต์ ํ ๊ฒฐ๊ณผ ๋น๊ต ๋ฐฑํ
์คํ
\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"AI ์ ๋น ๋ฑ๊ธ ํ์ ์ ์ ๋ขฐ๋์ ๋ฐ๋ผ ๊ฐ์ด ์น์ธยท์ ๋ฐ ํ๋
์ผ๋ก ๋๋๋ ๊ฒ์ฌ ์ด์์์ ๋น์ฉ์ผ๋ก ์ต์ ํํ๊ณ (์ ์ ์ ๋ฐ ํ๋
๊ดํ ๋๋น, ๊ฒ์ฌ๊ด ์ผ์ผ ํ๋ ์ ์ฝ ๋ฐ์), ๋ชจ๋ ํ์ ์ ๊ฒ์ฌ๊ด ์๋ ๋ณด์ ํ ์ ๋น๊ฐ๋
๊ด(์ฐฝ์ ๋นยท๋ณด์ ๊ฑด์ ํ์ง๋ณด์ฆ๊ด๊น์ง)์ด ์ง์ ์น์ธํ๋ 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 sklearn.ensemble import RandomForestClassifier, GradientBoostingRegressor\n",
"from sklearn.model_selection import cross_val_predict\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ฅ๋น ์ธ๊ด ์ฌ์ง ์์ฑ๊ธฐ (์ค์ ์ฅ๋นยท๋ถ๋ยท์์น ์ฌ์ง์ด ์๋๋ผ ๋์๋ก ๊ทธ๋ฆฐ 64x64 ํฉ์ฑ ์ด๋ฏธ์ง)\n",
"# ๋์ฅ๋ ๊ธ์ ํ๋ฉด์ ๊ท ์ด(crack)ยท๋ถ์(corrosion)ยทํ์(damage)ยท๋์ (leak)๋ฅผ ๊ทธ๋ฆฌ๊ณ , ์กฐ๋ช
ํธ์ฐจ์ ์งํ(mud) ์ค์ผ์ ์๋๋ค.\n",
"# ์ด๋ฏธ์ง์ ํจ๊ป ์ฅ๋น ๊ด๋ฆฌ ๊ธฐ๋ก(์ ์ ์์ฝ)๊ณผ ์ ๋ต ๊ฒฐํจ ๋ง์คํฌยท์์๋ยท์ ๋น ๋ฑ๊ธ์ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"from scipy import ndimage as ndi\n",
"\n",
"SZ = 64\n",
"CLASSES = [\"none\", \"crack\", \"corrosion\", \"damage\", \"leak\"]\n",
"KO_CLS = {\"none\": \"๊ฒฐํจ ์์\", \"crack\": \"๊ท ์ด\", \"corrosion\": \"๋ถ์\", \"damage\": \"ํ์\", \"leak\": \"๋์ \"}\n",
"CLS_W = {\"none\": 0.0, \"crack\": 1.4, \"corrosion\": 1.0, \"damage\": 1.6, \"leak\": 0.6} # ์์๋ ๊ณ์ฐ์ฉ ๊ฒฐํจ ์ ํ ๊ฐ์ค์น\n",
"GRADE_NAME = {1: \"ํ์ฅ์ ๋น\", 2: \"์ผ์ ์ ๋น\", 3: \"์ฐฝ์ ๋น\"}\n",
"YY, XX = np.mgrid[0:SZ, 0:SZ]\n",
"\n",
"\n",
"def grade_of(idx):\n",
" # [TODO : ํ์ ์ค์ต 7] ์ธ๊ด ํ์ ๊ฒฐ๊ณผ์ ๋ํ ๊ฒ์ฌ๊ด ์๋ ๋ณด์ ๋ฐ ์ต์ข
์ ๋น ๋ฑ๊ธ HITL ์น์ธ ์ ์ฐจ ์ค๊ณ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def blob(rng, cy, cx, r, rough=0.35):\n",
" \"\"\"๋ถ๊ท์นํ ์ํ ์์ญ ๋ง์คํฌ.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 7] ์ธ๊ด ํ์ ๊ฒฐ๊ณผ์ ๋ํ ๊ฒ์ฌ๊ด ์๋ ๋ณด์ ๋ฐ ์ต์ข
์ ๋น ๋ฑ๊ธ HITL ์น์ธ ์ ์ฐจ ์ค๊ณ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def draw(rng, cls, sev):\n",
" \"\"\"๊ฒฐํจ ์ ํยท์ฌ๊ฐ๋ sev(0~1)์ ๋ง๋ ์ ๋ต ๋ง์คํฌ์ ์ ์ ๋ณด๋ฅผ ๋ง๋ ๋ค.\"\"\"\n",
" m = np.zeros((SZ, SZ), bool)\n",
" if cls == \"crack\":\n",
" y, x, ang = rng.uniform(12, 52), rng.uniform(12, 52), rng.uniform(0, np.pi)\n",
" for _ in range(int(20 + 40 * sev)):\n",
" ang += rng.normal(0, 0.35); y += np.sin(ang) * 1.2; x += np.cos(ang) * 1.2\n",
" if 0 <= int(y) < SZ and 0 <= int(x) < SZ:\n",
" m[int(y), int(x)] = True\n",
" m = ndi.binary_dilation(m, iterations=1 + int(sev > 0.6))\n",
" elif cls == \"corrosion\":\n",
" for _ in range(int(rng.integers(1, 4))):\n",
" m |= blob(rng, rng.uniform(14, 50), rng.uniform(14, 50), 3 + 9 * sev * rng.uniform(0.6, 1.1))\n",
" elif cls == \"damage\":\n",
" m = blob(rng, rng.uniform(20, 44), rng.uniform(20, 44), 6 + 12 * sev, 0.3)\n",
" elif cls == \"leak\":\n",
" x0, y0 = rng.uniform(16, 48), rng.uniform(8, 24)\n",
" w, ln = 2 + 4 * sev, 12 + 26 * sev\n",
" m = (np.abs(XX - x0 - 0.15 * (YY - y0)) < w * (1 - 0.4 * (YY - y0) / ln)) & (YY > y0) & (YY < y0 + ln)\n",
" return m\n",
"\n",
"\n",
"def make_images(n=600, seed=0, mud=0.0, p_none=0.25, dark=False):\n",
" \"\"\"๊ฐ์ ์ธ๊ด ์ฌ์ง n์ฅ(+์งํ ์ค์ผ mud ๋น์จ)๊ณผ ๊ด๋ฆฌ ๊ธฐ๋กยท์ ๋ต. ๋ฐํ: meta(DataFrame), imgs(n,64,64,3), masks(n,64,64).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" imgs, masks, rows = np.zeros((n, SZ, SZ, 3), np.float32), np.zeros((n, SZ, SZ), bool), []\n",
" for i in range(n):\n",
" cls = \"none\" if rng.random() < p_none else str(rng.choice(CLASSES[1:], p=[0.25, 0.3, 0.25, 0.2]))\n",
" sev = 0.0 if cls == \"none\" else float(rng.beta(1.6, 2.0))\n",
" base = np.array([0.42, 0.46, 0.40]) * (1 + 0.5 * ndi.gaussian_filter(rng.normal(size=(SZ, SZ)), 8)[..., None] * 3)\n",
" img = base + rng.normal(0, 0.012, (SZ, SZ, 3))\n",
" m = draw(rng, cls, sev)\n",
" a = ndi.gaussian_filter(m.astype(float), 0.8)[..., None] # ๊ฒฝ๊ณ๋ฅผ ๋ถ๋๋ฝ๊ฒ ์๋๋ค\n",
" if cls == \"crack\":\n",
" col = img * rng.uniform(0.2, 0.45)\n",
" elif cls == \"corrosion\":\n",
" col = np.array([0.55, 0.30, 0.12]) * rng.uniform(0.75, 1.15) * (0.85 + 0.3 * rng.random((SZ, SZ, 1)))\n",
" elif cls == \"damage\":\n",
" col = np.ones_like(img) * np.array([0.34, 0.34, 0.36]) * (1.0 - 0.45 * sev) * rng.uniform(0.75, 1.0)\n",
" rim = m & ~ndi.binary_erosion(m, iterations=2)\n",
" a = np.maximum(a, 0.0); img = np.where(rim[..., None], 0.7, img)\n",
" elif cls == \"leak\":\n",
" col = np.ones_like(img) * np.array([0.10, 0.09, 0.08]) * rng.uniform(0.8, 2.6)\n",
" else:\n",
" col = img\n",
" img = img * (1 - a) + col * a\n",
" if rng.random() < 0.35: # ๋ฌดํดํ ์ผ๋ฃฉ(๊ฒฐํจ ์๋): ์
์ ๊ฐ์ ๋ฒ์ง\n",
" st = blob(rng, rng.uniform(5, 58), rng.uniform(5, 58), rng.uniform(6, 14), 0.5)\n",
" al = ndi.gaussian_filter(st.astype(float), 2.5)[..., None] * rng.uniform(0.15, 0.4)\n",
" img = img * (1 - al) + np.array([0.50, 0.40, 0.30]) * al\n",
" if rng.random() < 0.3: # ๋ฌดํดํ ๊ธด ์คํฌ๋์น(๋ฐ์ ์ )\n",
" y0, x0, ang = rng.uniform(5, 58), rng.uniform(5, 58), rng.uniform(0, np.pi)\n",
" for t in np.linspace(0, 25, 40):\n",
" yy, xx = int(y0 + t * np.sin(ang)), int(x0 + t * np.cos(ang))\n",
" if 0 <= yy < SZ and 0 <= xx < SZ:\n",
" img[yy, xx] = np.clip(img[yy, xx] * 1.5, 0, 1)\n",
" if mud > 0: # ์งํ ์ค์ผ: ๊ฒฐํจ ์๋ฅผ ๋ฎ์ ์ ์๋ค(์ ๋ต ๋ง์คํฌ๋ ๊ทธ๋๋ก)\n",
" mm = np.zeros((SZ, SZ), bool)\n",
" while mm.mean() < mud:\n",
" mm |= blob(rng, rng.uniform(0, SZ), rng.uniform(0, SZ), rng.uniform(5, 11), 0.5)\n",
" img[mm] = np.array([0.38, 0.30, 0.22]) * (0.85 + 0.3 * rng.random((mm.sum(), 1)))\n",
" gain = rng.uniform(0.3, 0.55) if dark else rng.uniform(0.5, 1.15)\n",
" ang = rng.uniform(0, 2 * np.pi)\n",
" light = gain * (1 + 0.3 * (np.cos(ang) * (XX - 32) + np.sin(ang) * (YY - 32)) / 32)\n",
" imgs[i] = np.clip(img * light[..., None] + rng.normal(0, 0.025, (SZ, SZ, 3)), 0, 1)\n",
" masks[i] = m\n",
" area = float(m.mean())\n",
" depth = {\"none\": 0.0, \"crack\": 0.3 + 3.0 * sev, \"corrosion\": 0.1 + 1.5 * sev, \"damage\": 1.0 + 8.0 * sev, \"leak\": 0.0}[cls] * rng.lognormal(0, 0.1)\n",
" idx = float(np.clip(100 * (1 - np.exp(-(CLS_W[cls] * area * 9 + 0.05 * depth))) + rng.normal(0, 1.5), 0, 100))\n",
" rows.append({\"img_id\": f\"IMG{seed:02d}{i:04d}\", \"cls\": cls, \"sev\": sev, \"area_frac\": area, \"depth_mm\": depth, \"damage_idx\": idx,\n",
" \"grade\": int(grade_of(idx)), \"gain\": gain, \"mud\": mud})\n",
" meta = pd.DataFrame(rows)\n",
" # ์ฅ๋น ๊ด๋ฆฌ ๊ธฐ๋ก(์ ์ ์์ฝ): ์ฌ์ง ํ์ ๊ณผ ๋์กฐํ ๊ฐ\n",
" age = rng.integers(1, 25, n)\n",
" meta[\"day\"] = np.sort(rng.integers(0, 90, n))\n",
" meta[\"equip_type\"] = rng.choice([\"vehicle\", \"generator\", \"comm\", \"trailer\"], n, p=[0.4, 0.2, 0.2, 0.2])\n",
" meta[\"mfg_year\"] = 2026 - age\n",
" meta[\"op_hours\"] = np.clip(rng.normal(180 * age, 400, n) + 8 * meta[\"damage_idx\"], 10, 20000)\n",
" meta[\"env\"] = rng.choice([\"coastal\", \"inland\"], n, p=[0.3, 0.7])\n",
" reg = np.clip(meta[\"grade\"] + rng.choice([-1, 0, 0, 0, 0, 0, 1, 2], n), 1, 3) # ์ฅ๋น ๊ด๋ฆฌ ๊ธฐ๋ก์ ์ง์ ์ ๋น ๋ฑ๊ธ\n",
" meta[\"reg_grade\"] = reg\n",
" meta[\"emergency\"] = rng.random(n) < 0.04 # ๊ธด๊ธ ์ ๊ฒ ์์ฒญ(ํ์ง ๋ถ๋์ด์ด๋ ๋ณด์กด)\n",
" return meta, imgs, masks\n",
"# ---- ์ ์ฒ๋ฆฌ(์กฐ๋ช
๋ณด์ ยทํ์คํ)์ ๊ฒฐํจ ํ๋ณด ๋ถํ , ํน์ง ์ถ์ถ (cv2 ๋์ numpyยทscipy ์ฌ์ฉ) ----\n",
"def correct(img):\n",
" \"\"\"์กฐ๋ช
๋ณด์ : ๋ฐ๊ธฐ์ ํ๋ฉด(์กฐ๋ช
๊ธฐ์ธ๊ธฐ)์ ์ ํฉํด ๋๋๋ค. ์ด๋์ด ๊ฒฐํจ ํ์๋ ์ ์ธํ๊ณ ๋ ๋ฒ ๋ฐ๋ณต ์ ํฉํ๋ค. ๋ฐํ: ๋ณด์ ๋ฐ๊ธฐ(์ ์ ๋ฉด ์ฝ 1), ์ ๋น์จ.\"\"\"\n",
" g = img.mean(axis=2)\n",
" A = np.c_[np.ones(SZ * SZ), XX.ravel() / SZ, YY.ravel() / SZ]\n",
" keep = np.ones(SZ * SZ, bool)\n",
" for _ in range(3):\n",
" coef, *_ = np.linalg.lstsq(A[keep], g.ravel()[keep], rcond=None)\n",
" plane = (A @ coef).reshape(SZ, SZ)\n",
" keep = (np.abs(g - plane) < 0.08 * np.maximum(plane, 1e-3)).ravel()\n",
" if keep.sum() < 200:\n",
" keep[:] = True\n",
" c = g / np.maximum(plane, 1e-3)\n",
" chroma = img[..., 0] / (img.sum(axis=2) + 1e-6)\n",
" return c, chroma\n",
"\n",
"\n",
"SEGP = {\"dark\": 0.7, \"rust\": 0.5, \"rim\": 1.45} # ๋ถํ ์๊ณ๊ฐ(์ด๋์ ๋ณด์ ๋ฐ๊ธฐ, ๋ถ์ ๋น์จ, ๋ฐ์ ํ
๋๋ฆฌ): ์ฌ๋ณด์ ๋์\n",
"\n",
"\n",
"def segment(img):\n",
" \"\"\"๊ฒฐํจ ํ๋ณด ๋ง์คํฌ: ์ด๋์ด ์์ญ(๊ท ์ดยท๋์ ยทํ์) + ๋ถ์ ๊ฐ์ ์์ญ(๋ถ์) + ๋ฐ์ ํ
๋๋ฆฌ(ํ์).\"\"\"\n",
" c, chroma = correct(img)\n",
" dark = c < SEGP[\"dark\"]\n",
" rust = (chroma > SEGP[\"rust\"]) & (c > 0.45)\n",
" rim = c > SEGP[\"rim\"]\n",
" m = dark | rust | ndi.binary_dilation(rim, iterations=1) & ndi.binary_dilation(dark, iterations=3)\n",
" m = ndi.binary_opening(m, structure=np.ones((2, 2))) if m.sum() > 12 else m\n",
" return m, c, chroma, dark, rust, rim\n",
"\n",
"\n",
"def extract(img):\n",
" \"\"\"๋ถํ ๊ฒฐ๊ณผ์ ์ยท๋ชจ์์์ 24๊ฐ ํน์ง์ ๋ง๋ ๋ค.\"\"\"\n",
" m, c, chroma, dark, rust, rim = segment(img)\n",
" lab, n = ndi.label(m)\n",
" area = float(m.sum())\n",
" if n:\n",
" sizes = ndi.sum(m, lab, range(1, n + 1))\n",
" k = int(np.argmax(sizes)) + 1\n",
" sl = ndi.find_objects(lab)[k - 1]\n",
" h, w = sl[0].stop - sl[0].start, sl[1].stop - sl[1].start\n",
" big = float(sizes.max())\n",
" elong, fill = max(h, w) / max(min(h, w), 1), big / (h * w)\n",
" else:\n",
" big, elong, fill = 0.0, 1.0, 0.0\n",
" gy, gx = np.gradient(c)\n",
" mag = np.hypot(gx, gy)\n",
" inside = m.sum() > 0\n",
" return {\"area_px\": area, \"area_frac\": area / SZ ** 2, \"n_comp\": float(n), \"big_px\": big, \"elong\": elong, \"fill\": fill,\n",
" \"dark_frac\": float(dark.mean()), \"rust_frac\": float(rust.mean()), \"rim_frac\": float(rim.mean()),\n",
" \"c_mean\": float(c.mean()), \"c_std\": float(c.std()), \"c_min\": float(c.min()), \"edge_mean\": float(mag.mean()), \"edge_p95\": float(np.quantile(mag, 0.95)),\n",
" \"chroma_mean\": float(chroma.mean()), \"chroma_max\": float(chroma.max()),\n",
" \"in_dark\": float(c[m].mean()) if inside else 1.0, \"in_chroma\": float(chroma[m].mean()) if inside else float(chroma.mean()),\n",
" \"ring_contrast\": float(c[ndi.binary_dilation(m, iterations=3) & ~m].mean() - c[m].mean()) if inside and (~m).any() else 0.0,\n",
" \"thin\": float(1 - fill), \"gain\": float(img.mean()), \"sat\": float((img.max(axis=2) - img.min(axis=2)).mean()),\n",
" \"cy\": float(ndi.center_of_mass(m)[0] / SZ) if inside else 0.5, \"cx\": float(ndi.center_of_mass(m)[1] / SZ) if inside else 0.5}\n",
"\n",
"\n",
"def feature_table(imgs):\n",
" return pd.DataFrame([extract(im) for im in imgs])\n",
"# ---- ํ์ ํ์ดํ๋ผ์ธ: ๊ฒฐํจ ์ ํ ๋ถ๋ฅ + ์์๋ ํ๊ท(๋ถ๋ฅ ํ๋ฅ ํฌํจ) -> ์ ๋น ๋ฑ๊ธ ----\n",
"def fit_pipeline(meta, F):\n",
" \"\"\"๊ฒฐํจ ์ ํ ๋ถ๋ฅ๊ธฐ์ ์์๋ ํ๊ท๊ธฐ๋ฅผ ํ์ตํ๋ค. ํ๊ท๊ธฐ ์
๋ ฅ์ ๋ถ๋ฅ ํ๋ฅ ์ ๊ต์ฐจ ์์ธก์ผ๋ก ๋ง๋ค์ด ๋์ค์ ๋ง๋๋ค.\"\"\"\n",
" clf = RandomForestClassifier(n_estimators=150, random_state=0, n_jobs=1)\n",
" P = cross_val_predict(clf, F, meta[\"cls\"], cv=4, method=\"predict_proba\")\n",
" clf.fit(F, meta[\"cls\"])\n",
" reg = GradientBoostingRegressor(n_estimators=200, max_depth=3, learning_rate=0.06, random_state=0).fit(np.c_[F.values, P], meta[\"damage_idx\"])\n",
" return {\"clf\": clf, \"reg\": reg, \"cols\": list(F.columns)}\n",
"\n",
"\n",
"def predict_pipeline(pl, F):\n",
" X = F[pl[\"cols\"]]\n",
" P = pl[\"clf\"].predict_proba(X)\n",
" idx = np.clip(pl[\"reg\"].predict(np.c_[X.values, P]), 0, 100)\n",
" return pl[\"clf\"].classes_[P.argmax(axis=1)], P, idx, P.max(axis=1)\n",
"# ---- ์ฌ์ง ํ์ง ๊ฒ์ดํธ: ์งํยท์ค์ผ ๊ฐ๋ฆผ ์ถ์ ๊ณผ ํ์ ์ ๋ขฐ๋ ์ง์ ----\n",
"def mud_est(img):\n",
" \"\"\"์งํ(๊ฐ์, ์ฑ๋ ๋ฎ์ ๋ถ์ ๊ธฐ) ํ์ ๋น์จ์ ์ถ์ ํ๋ค. 3x3 ํ๊ท ์ผ๋ก ์ก์์ ์ค์ธ ๋ค ์ ๋น์จ๋ก ํ๋จํ๋ฉฐ, ์ค์ ๊ฐ๋ฆผ ๋น์จ๋ณด๋ค ์๊ฒ ๋์ค์ง๋ง ์์๋ ์ ์ง๋๋ค.\"\"\"\n",
" sm = ndi.uniform_filter(img, size=(3, 3, 1))\n",
" s = sm.sum(axis=2) + 1e-6\n",
" rr, gg = sm[..., 0] / s, sm[..., 1] / s\n",
" return float(((rr > 0.385) & (rr < 0.46) & (gg < 0.325)).mean())\n",
"\n",
"\n",
"def confidence(P, idx, F, mud):\n",
" \"\"\"ํ์ ์ ๋ขฐ๋ ์ง์ 0~100 = 50 x ๋ถ๋ฅ ํ๋ฅ ์ต๋๊ฐ + 35 x ๋ฑ๊ธ ๊ฒฝ๊ณ๊น์ง ๊ฑฐ๋ฆฌ(8์ ์์ ํฌํ) + 15 x ์ฌ์ง ํ์ง. ์ค์ผ ์์ฌ(mud ์ถ์ 1% ์ด์)์ด๋ฉด ์ํ 75๋ก ์ ํํ๋ค.\"\"\"\n",
" bd = np.minimum(np.abs(idx - 35), np.abs(idx - 65))\n",
" qual = np.clip(1.0 - np.clip(0.55 - F[\"gain\"].values, 0, None) * 2 - np.clip(0.9 - F[\"c_mean\"].values, 0, None), 0, 1)\n",
" c = 50 * P.max(axis=1) + 35 * np.clip(bd / 8, 0, 1) + 15 * qual\n",
" return np.where(np.asarray(mud) >= 0.01, np.minimum(c, 75.0), c)\n",
"\n",
"mtr, itr, _ = make_images(600, seed=1)\n",
"Ftr = feature_table(itr)\n",
"pl = fit_pipeline(mtr, Ftr)\n",
"\n",
"\n",
"def judge(seed, n, **kw):\n",
" \"\"\"์ฌ์ง n์ฅ์ ๋ง๋ค์ด AI ํ์ (๋ฑ๊ธยท์ ๋ขฐ๋)๊ณผ ์ ๋ต ๋ฑ๊ธ์ ๋๋ ค์ค๋ค.\"\"\"\n",
" m, im, _ = make_images(n, seed=seed, **kw)\n",
" F = feature_table(im)\n",
" cls, P, idx, _ = predict_pipeline(pl, F)\n",
" conf = confidence(P, idx, F, [mud_est(x) for x in im])\n",
" return pd.DataFrame({\"true\": m[\"grade\"].values, \"ai\": grade_of(idx), \"conf\": conf, \"idx\": idx, \"cls\": cls, \"id\": m[\"img_id\"].values})\n",
"\n",
"\n",
"SETS = {\"ํ์(์ ์ ์กฐ๋ช
)\": judge(2, 300), \"์ ์กฐ๋ ์ฌ์ง ๋ง์\": judge(7, 150, dark=True), \"์งํ ๊ฐ๋ฆผ 20%\": judge(8, 150, mud=0.2)}\n",
"\n",
"# ---------------- 1) ๋น์ฉ ๋ชจ๋ธ(๊ต์ก์ฉ ๊ฐ์ , ๋ง์/์ฅ) ----------------\n",
"C_FULL, C_QUICK, C_AI = 0.9, 0.25, 0.05 # ์ ๋ฐ ํ๋
, ๊ฐ์ด ์น์ธ(AI ํ์ ์ด๋ ํ ์น์ธ), AI ํ์ ์ ์ฐ ๋น์ฉ\n",
"P_HUMAN, P_CATCH = 0.08, 0.5 # ๊ฒ์ฌ๊ด ์ ๋ฐ ํ๋
์ค๋ฅ์จ(+-1 ๋ฑ๊ธ), ๊ฐ์ด ์น์ธ ์ AI ์คํ์ ๊ฑธ๋ฌ๋ด๋ ํ๋ฅ \n",
"LOSS_UNDER, LOSS_OVER = 20.0, 6.0 # ๊ณผ์ ํ์ (์ฌ์
๊ณ ยท๊ณ ์ฅ ์ํ) / ๊ณผ๋ ํ์ (๊ณผ๋ค ์ ๋น) 1๋ฑ๊ธ๋น ์์ค\n",
"CAP = 120 # ๊ฒ์ฌ๊ด ์ผ์ผ ์ ๋ฐ ํ๋
ํ๋(์ฅ)\n",
"\n",
"\n",
"def loss(true, grade):\n",
" d = grade - true\n",
" return np.where(d < 0, -d * LOSS_UNDER, d * LOSS_OVER)\n",
"\n",
"\n",
"def operate(df, T, cap=CAP, seed=0):\n",
" \"\"\"์ ๋ขฐ๋ T ์ด์์ ๊ฐ์ด ์น์ธ, ๋ฏธ๋ง์ ์ ๋ฐ ํ๋
(๊ฒ์ฌ๊ด ๋ณด์ ). ์ ๋ฐ ํ๋
ํ๋๋ฅผ ๋์ผ๋ฉด ์ ๋ขฐ๋๊ฐ ๋ฎ์ ์์๋๋ก๋ง ์ ๋ฐ ํ๋
ํ๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" n = len(df)\n",
" full = (df[\"conf\"] < T).values\n",
" if full.sum() > cap:\n",
" order = np.argsort(df[\"conf\"].values)\n",
" full = np.zeros(n, bool); full[order[:cap]] = True\n",
" final = df[\"ai\"].values.copy()\n",
" err = rng.random(n) < P_HUMAN\n",
" human = np.clip(df[\"true\"].values + np.where(err, rng.choice([-1, 1], n), 0), 1, 3)\n",
" final[full] = human[full]\n",
" wrong_ai = (df[\"ai\"].values != df[\"true\"].values) & ~full\n",
" caught = wrong_ai & (rng.random(n) < P_CATCH)\n",
" final[caught] = df[\"true\"].values[caught]\n",
" cost = full.sum() * C_FULL + (~full).sum() * C_QUICK + n * C_AI + loss(df[\"true\"].values, final).sum()\n",
" return cost, full.mean(), (final == df[\"true\"].values).mean(), int(((final < df[\"true\"].values)).sum())\n",
"\n",
"\n",
"def practice(df, seed=0):\n",
" \"\"\"๊ดํ: ๋ชจ๋ ์ฌ์ง์ ๊ฒ์ฌ๊ด์ด ์ ๋ฐ ํ๋
(ํ๋ ์์, ๊ฒ์ฌ๊ด ์ค๋ฅ์จ P_HUMAN).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" err = rng.random(len(df)) < P_HUMAN\n",
" final = np.clip(df[\"true\"].values + np.where(err, rng.choice([-1, 1], len(df)), 0), 1, 3)\n",
" return len(df) * C_FULL + loss(df[\"true\"].values, final).sum(), (final == df[\"true\"].values).mean()\n",
"\n",
"\n",
"rows = []\n",
"for name, df in SETS.items():\n",
" cp, ap = practice(df)\n",
" co, fr, ao, un = operate(df, 80)\n",
" ai_only = len(df) * C_AI + loss(df[\"true\"].values, df[\"ai\"].values).sum()\n",
" rows.append((name, len(df), cp, ai_only, co, 1 - co / cp, fr, ap, ao, un))\n",
"res = pd.DataFrame(rows, columns=[\"์๋๋ฆฌ์ค\", \"์ฅ์\", \"๊ดํ ์ ์ ํ๋
๋น์ฉ\", \"AI ๋จ๋
๋น์ฉ(๊ธ์ง์)\", \"HITL ๋น์ฉ(T=80)\", \"์ ๊ฐ๋ฅ \", \"์ ๋ฐ ํ๋
๋น์จ\", \"๊ดํ ์ผ์น์จ\", \"HITL ์ผ์น์จ\", \"HITL ๊ณผ์ ํ์ \"])\n",
"print(\"[1] ์๋๋ฆฌ์ค๋ณ ๊ฒ์ฌ ์ด์ ๋น์ฉ(๊ฐ์ ๋จ๊ฐ, ๋ง์)\")\n",
"print(res.assign(์ ๊ฐ๋ฅ =res[\"์ ๊ฐ๋ฅ \"].map(\"{:.1%}\".format), **{\"์ ๋ฐ ํ๋
๋น์จ\": res[\"์ ๋ฐ ํ๋
๋น์จ\"].map(\"{:.0%}\".format), \"๊ดํ ์ผ์น์จ\": res[\"๊ดํ ์ผ์น์จ\"].map(\"{:.1%}\".format), \"HITL ์ผ์น์จ\": res[\"HITL ์ผ์น์จ\"].map(\"{:.1%}\".format)}).round(1).to_string(index=False))\n",
"r0 = res.iloc[0]\n",
"passed = [f\"{r['์๋๋ฆฌ์ค']} {r['์ ๊ฐ๋ฅ ']:.1%}\" for _, r in res.iterrows() if r[\"์ ๊ฐ๋ฅ \"] >= .15]\n",
"print(f\" ํ์ ์ ๊ฐ {r0['์ ๊ฐ๋ฅ ']:.1%} (๋ชฉํ 15% ์ด์: {'๋ฌ์ฑ' if r0['์ ๊ฐ๋ฅ '] >= .15 else '๋ฏธ๋ฌ'}), 15% ์ด์ ๋ฌ์ฑ ์๋๋ฆฌ์ค: {', '.join(passed)}\")\n",
"print(f\" AI ๋จ๋
๋น์ฉ์ ํ์ {r0['AI ๋จ๋
๋น์ฉ(๊ธ์ง์)']:.0f}, ์งํ {res.iloc[2]['AI ๋จ๋
๋น์ฉ(๊ธ์ง์)']:.0f}๋ก ์ค์ผ ์ฌ์ง์์ ๊ณผ์ ํ์ ์์ค์ด ํญ์ฆํ๋ฏ๋ก ํ์ฉํ์ง ์๋๋ค(๋ชจ๋ ํ์ ์ ์ฌ๋์ด ์น์ธ). ์งํ 20%๋ ์ ๋ฐ ํ๋
์ด ๋์ด ์ ๊ฐ์ด ๋์ง ์๋๋ค(๋ฏธ๋ฌ).\")\n",
"\n",
"# ---------------- 2) ์ ๋ขฐ๋ ๊ธฐ์ค T ํ์๊ณผ ๊ฒ์ฌ๊ด ํ๋ ----------------\n",
"sw = []\n",
"for T in (60, 70, 80, 90, 95):\n",
" c, fr, a, un = operate(SETS[\"ํ์(์ ์ ์กฐ๋ช
)\"], T)\n",
" sw.append((T, c, fr, a, un))\n",
"sw = pd.DataFrame(sw, columns=[\"๊ธฐ์ค T\", \"๋น์ฉ\", \"์ ๋ฐ ํ๋
๋น์จ\", \"์ผ์น์จ\", \"๊ณผ์ ํ์ \"])\n",
"print(\"\\n[2] ์ ๋ขฐ๋ ๊ธฐ์ค T ๋ฏผ๊ฐ๋(ํ์ 300์ฅ)\\n\" + sw.round(3).to_string(index=False))\n",
"best = sw.loc[sw[\"๋น์ฉ\"].idxmin()]\n",
"print(f\" ๋น์ฉ ์ต์ ๊ธฐ์ค T={int(best['๊ธฐ์ค T'])}. ๊ธฐ์ค์ ๋๋ฌด ๋์ด๋ฉด ์ ๋ฐ ํ๋
์ด ๋์ด ๋น์ฉ์ด ์ค๋ฅด๊ณ , ๋๋ฌด ๋ฎ์ผ๋ฉด AI ์คํ์ด ํต๊ณผํ๋ค.\")\n",
"cp, _ = practice(SETS[\"์งํ ๊ฐ๋ฆผ 20%\"])\n",
"for cap in (150, 60, 20):\n",
" c, fr, a, un = operate(SETS[\"์งํ ๊ฐ๋ฆผ 20%\"], 80, cap=cap)\n",
" print(f\" ์งํ 20% ์๋๋ฆฌ์ค, ๊ฒ์ฌ๊ด ํ๋ {cap}์ฅ/์ผ: ๋น์ฉ {c:.0f} (๊ดํ ๋๋น ์ ๊ฐ {1 - c / cp:+.1%}), ์ ๋ฐ ํ๋
{fr:.0%}, ์ผ์น์จ {a:.1%}, ๊ณผ์ ํ์ {un}์ฅ\")\n",
"print(\" ๊ฒ์ฌ๊ด ํ๋๊ฐ ์ค๋ฉด ์ ๋ขฐ๋ ๋ฎ์ ์ฌ์ง๊น์ง ๊ฐ์ด ์น์ธ์ผ๋ก ๋์ด๊ฐ AI ์คํ์ด ๊ฑธ๋ฌ์ง์ง ์์ ์์ค์ด ์ปค์ง๋ค(๋น์ฉ +). ์ค์ผ ์ฌ์ง์ ์ฌ์ดฌ์ยท์ฌ์ ์ธ์ฒ ์๋ด๋ก ์ค์ฌ์ผ ํ๋ค.\")\n",
"\n",
"# ---------------- 3) HITL ๊ฒฐ์ฌ ์ํฌํ๋ก์ฐ ----------------\n",
"USERS = {\"ai_bot\": \"AI\", \"ins_kim\": \"INS\", \"ins_lee\": \"INS\", \"sup_park\": \"SUP\", \"qa_choi\": \"QA\", \"aud_han\": \"AUD\"}\n",
"PERM = {\"AI\": {\"submit\"}, \"INS\": {\"review\", \"correct\", \"reject\"}, \"SUP\": {\"approve\", \"reject\"}, \"QA\": {\"approve\", \"reject\"}, \"AUD\": {\"read\"}}\n",
"\n",
"\n",
"class Flow:\n",
" def __init__(self):\n",
" self.docs, self.log, self.blocked = {}, [], []\n",
"\n",
" def _audit(self, user, action, doc, detail):\n",
" prev = self.log[-1][\"hash\"] if self.log else \"GENESIS\"\n",
" rec = {\"seq\": len(self.log) + 1, \"user\": user, \"action\": action, \"doc\": doc, \"detail\": detail, \"prev\": prev}\n",
" rec[\"hash\"] = hashlib.sha256(json.dumps(rec, ensure_ascii=False, sort_keys=True).encode()).hexdigest()[:12]\n",
" self.log.append(rec)\n",
"\n",
" def verify(self):\n",
" prev = \"GENESIS\"\n",
" for r in self.log:\n",
" body = {k: v for k, v in r.items() if k != \"hash\"}\n",
" if r[\"prev\"] != prev or hashlib.sha256(json.dumps(body, ensure_ascii=False, sort_keys=True).encode()).hexdigest()[:12] != r[\"hash\"]:\n",
" return False, r[\"seq\"]\n",
" prev = r[\"hash\"]\n",
" return True, None\n",
"\n",
" def deny(self, user, act, doc, why):\n",
" self.blocked.append((user, act, doc, why)); self._audit(user, \"DENIED:\" + act, doc, why); return False\n",
"\n",
" def submit(self, user, pid, grade, conf, parent=None):\n",
" if \"submit\" not in PERM.get(USERS.get(user), set()):\n",
" return self.deny(user, \"submit\", \"-\", \"AI ํ์ ๋ฑ๋ก ๊ถํ ์์\")\n",
" did = f\"INS{len(self.docs) + 1:02d}\"\n",
" self.docs[did] = {\"photo\": pid, \"ai\": grade, \"grade\": grade, \"conf\": conf, \"status\": \"๊ฒ์ฌ๋๊ธฐ\", \"line\": [\"INS\", \"SUP\"], \"step\": 0, \"done\": [], \"parent\": parent}\n",
" self._audit(user, \"submit\", did, f\"{pid} AI ๋ฑ๊ธ {GRADE_NAME[grade]} ์ ๋ขฐ๋ {conf:.0f}\")\n",
" return did\n",
"\n",
" def decide(self, user, did, act, reason=\"\", new_grade=None):\n",
" role, d = USERS.get(user), self.docs.get(did)\n",
" if role is None: return self.deny(user, act, did, \"๋ฏธ๋ฑ๋ก ์ฌ์ฉ์\")\n",
" if d is None or d[\"status\"] in (\"ํ์ \", \"๋ฐ๋ ค\"): return self.deny(user, act, did, \"๊ฒฐ์ฌ ๋์ ์๋(์ฒ๋ฆฌ ์๋ฃ)\")\n",
" if act not in PERM[role]: return self.deny(user, act, did, f\"{role} ์ญํ ์ {act} ๋ถ๊ฐ\")\n",
" if d[\"line\"][d[\"step\"]] != role: return self.deny(user, act, did, \"๊ฒฐ์ฌ ์์ ์๋ฐ\")\n",
" if user in d[\"done\"]: return self.deny(user, act, did, \"๊ธฐ๊ฒฐ์ฌ์ ์ฌ๊ฒฐ์ฌ ๊ธ์ง\")\n",
" if len(reason.strip()) < 10: return self.deny(user, act, did, \"์ฌ์ 10์ ์ด์ ํ์\")\n",
" if act == \"reject\":\n",
" d[\"status\"] = \"๋ฐ๋ ค\"\n",
" else:\n",
" if act == \"correct\":\n",
" d[\"grade\"] = new_grade\n",
" if new_grade != d[\"ai\"] and \"QA\" not in d[\"line\"]:\n",
" d[\"line\"] = d[\"line\"] + [\"QA\"] # ๊ฒ์ฌ๊ด์ด AI ํ์ ์ ๊ณ ์น๋ฉด ํ์ง๋ณด์ฆ๊ด๊น์ง ๊ฒฐ์ฌ\n",
" if d[\"grade\"] == 3 and \"QA\" not in d[\"line\"]:\n",
" d[\"line\"] = d[\"line\"] + [\"QA\"] # ์ฐฝ์ ๋น ํ์ ์ ํ์ง๋ณด์ฆ๊ด๊น์ง ๊ฒฐ์ฌ\n",
" d[\"done\"].append(user); d[\"step\"] += 1\n",
" d[\"status\"] = \"ํ์ \" if d[\"step\"] == len(d[\"line\"]) else (\"๊ฐ๋
๋๊ธฐ\" if d[\"step\"] >= 1 else \"๊ฒ์ฌ๋๊ธฐ\")\n",
" self._audit(user, act, did, reason + (f\" -> {GRADE_NAME[new_grade]}\" if new_grade else \"\")); return True\n",
"\n",
" def edit(self, user, did):\n",
" if self.docs[did][\"status\"] == \"ํ์ \": return self.deny(user, \"edit\", did, \"ํ์ ํ ๋ฑ๊ธ ๋ณ๊ฒฝ ๊ธ์ง(์ฌ๊ฒ์ฌ ๋ฑ๋ก)\")\n",
" return True\n",
"\n",
"\n",
"base = SETS[\"ํ์(์ ์ ์กฐ๋ช
)\"]\n",
"mix = pd.concat([base[base.conf >= 80].head(3), base[base.conf < 80].head(4), base[base.ai == 3].head(2)]).drop_duplicates(\"id\").head(8).reset_index(drop=True)\n",
"wf = Flow()\n",
"ids = [wf.submit(\"ai_bot\", r.id, int(r.ai), r.conf) for r in mix.itertuples()]\n",
"print(f\"\\n[3] AI ํ์ {len(ids)}๊ฑด ์ ๋ถ '๊ฒ์ฌ๋๊ธฐ' ๋ฑ๋ก, ์๋ ํ์ {sum(wf.docs[i]['status'] == 'ํ์ ' for i in ids)}๊ฑด (๋ชฉํ 0๊ฑด)\")\n",
"low = [i for i, r in zip(ids, mix.itertuples()) if r.conf < 80][0]\n",
"first = ids[0]\n",
"wf.decide(\"ins_kim\", first, \"review\", \"AI ํ์ ์ด๋, ์ฌ์ง๊ณผ ์ผ์น ํ์ธํจ\")\n",
"wf.decide(\"sup_park\", first, \"approve\", \"๊ฒ์ฌ๊ด ์๊ฒฌ ํ์ธ, ๋ฑ๊ธ ํ์ \")\n",
"wf.decide(\"ins_lee\", low, \"correct\", \"๊ฒฝ๊ณ ๋ถ๊ทผ ๊ฒฐํจ, ๋ฐ์ค ์ฌ์ธก์ ํ ๋ฑ๊ธ ์กฐ์ \", new_grade=min(3, wf.docs[low][\"ai\"] + 1))\n",
"wf.decide(\"sup_park\", low, \"approve\", \"๋ณด์ ์ฌ์ ํ๋น, ํ์ง๋ณด์ฆ ์์ \")\n",
"wf.decide(\"qa_choi\", low, \"approve\", \"๋ณด์ ๊ทผ๊ฑฐ ์ฌ์ง ๋์กฐ ์๋ฃ\")\n",
"rj = ids[2]\n",
"wf.decide(\"ins_kim\", rj, \"reject\", \"์ค์ผ ์ฌ์ง์ผ๋ก ํ๋
๋ถ๊ฐ, ์ฌ์ดฌ์ ์๊ตฌ\")\n",
"new = wf.submit(\"ai_bot\", mix.id[2], int(mix.ai[2]), float(mix.conf[2]), parent=rj)\n",
"tgt = ids[-1]\n",
"attacks = [(\"ai_bot\", tgt, \"review\", \"AI ๊ณ์ ์ด ์ง์ ๊ฒํ ์ฒ๋ฆฌํจ\"), (\"sup_park\", tgt, \"approve\", \"๊ฒ์ฌ๊ด ๊ฒํ ์์ด ๊ฐ๋
๊ด์ด ์น์ธํจ\"), (\"aud_han\", tgt, \"approve\", \"๊ฐ์ฌ ๊ณ์ ์ผ๋ก ์น์ธ ์๋ํจ\"),\n",
" (\"ghost\", tgt, \"review\", \"์ธ๋ถ ๊ณ์ ์ผ๋ก ๊ฒํ ์๋ํจ\"), (\"ins_kim\", tgt, \"review\", \"\"), (\"ins_kim\", tgt, \"correct\", \"์งง์\"), (\"qa_choi\", tgt, \"approve\", \"ํ์ง๋ณด์ฆ๊ด์ด ์์๋ฅผ ๊ฑด๋๋ฐ์ด ์น์ธ\"),\n",
" (\"ins_kim\", first, \"review\", \"์ด๋ฏธ ํ์ ๋ ๊ฑด์ ๋ค์ ๊ฒํ ํจ\"), (\"sup_park\", low, \"reject\", \"ํ์ ๋ ๊ฑด์ ๋ค์ง์ด ๋ฐ๋ คํจ\")]\n",
"for u, doc, a, rs in attacks:\n",
" wf.decide(u, doc, a, rs, new_grade=2 if a == \"correct\" else None)\n",
"wf.edit(\"ins_kim\", first)\n",
"print(f\" ๊ถํ ์ฐํ ์๋ 10๊ฑด ์ค ์ฐจ๋จ {len(wf.blocked)}๊ฑด (์ฐจ๋จ์จ {len(wf.blocked) / 10:.0%}, ๋ชฉํ 100%)\")\n",
"for u, a, doc, why in wf.blocked[:4]:\n",
" print(f\" ์ฐจ๋จ {u:<9} {a:<8} {doc}: {why}\")\n",
"ok, _ = wf.verify()\n",
"snap = json.loads(json.dumps(wf.log))\n",
"wf.log[3][\"detail\"] = \"์ฌํ์ ๊ณ ์ณ ์ด ์ฌ์ \"\n",
"ok2, at = wf.verify()\n",
"wf.log = snap\n",
"print(f\" ๊ฐ์ฌ ๋ก๊ทธ {len(wf.log)}๊ฑด ํด์ ์ฒด์ธ ๋ฌด๊ฒฐ์ฑ {'100%' if ok else 'ํผ์'}; 4๋ฒ ๊ธฐ๋ก ์ฌํ ์์ ์ {'ํต๊ณผ' if ok2 else f'์๋ณ์กฐ ๊ฒ์ถ({at}๋ฒ)'}\")\n",
"print(f\" {low}: AI {GRADE_NAME[wf.docs[low]['ai']]} -> ๊ฒ์ฌ๊ด ๋ณด์ {GRADE_NAME[wf.docs[low]['grade']]}(๋ณด์ ์ ํ์ง๋ณด์ฆ๊ด๊น์ง 3๋จ๊ณ ๊ฒฐ์ฌ, ์ํ {wf.docs[low]['status']}); {rj}(๋ฐ๋ ค) -> {new}(์ฌ๋ฑ๋ก, parent ์ฐ๊ฒฐ)\")\n",
"print(\" ๋ฌธ์ ์ํ:\", pd.Series({k: v[\"status\"] for k, v in wf.docs.items()}).value_counts().to_dict(), \"| ์ต์ข
์ ๋น ๋ฑ๊ธ์ ๊ฒ์ฌ๊ดยท์ ๋น๊ฐ๋
๊ด์ ๊ฒฐ์ฌ๋ก ํ์ ๋๋ฉฐ AI๋ ํ์ ๊ถํ์ด ์๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(12, 4))\n",
"x = np.arange(len(res)); ax[0].bar(x - .2, res[\"๊ดํ ์ ์ ํ๋
๋น์ฉ\"], .4, label=\"full inspection\"); ax[0].bar(x + .2, res[\"HITL ๋น์ฉ(T=80)\"], .4, label=\"HITL (T=80)\")\n",
"ax[0].set_xticks(x); ax[0].set_xticklabels([f\"S{i + 1}\" for i in x]); ax[0].legend(); ax[0].set_title(\"Total cost by scenario (10k KRW)\")\n",
"ax[1].plot(sw[\"๊ธฐ์ค T\"], sw[\"๋น์ฉ\"], \"o-\"); ax[1].set_title(\"Cost vs confidence threshold T\"); ax[1].set_xlabel(\"T\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
07 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[1] ์๋๋ฆฌ์ค๋ณ ๊ฒ์ฌ ์ด์ ๋น์ฉ(๊ฐ์ ๋จ๊ฐ, ๋ง์)\n",
" ์๋๋ฆฌ์ค ์ฅ์ ๊ดํ ์ ์ ํ๋
๋น์ฉ AI ๋จ๋
๋น์ฉ(๊ธ์ง์) HITL ๋น์ฉ(T=80) ์ ๊ฐ๋ฅ ์ ๋ฐ ํ๋
๋น์จ ๊ดํ ์ผ์น์จ HITL ์ผ์น์จ HITL ๊ณผ์ ํ์ \n",
"ํ์(์ ์ ์กฐ๋ช
) 300 442.0 379.0 178.9 59.5% 22% 94.3% 99.0% 2\n",
"์ ์กฐ๋ ์ฌ์ง ๋ง์ 150 263.0 219.5 189.0 28.2% 55% 92.0% 94.7% 3\n",
"์งํ ๊ฐ๋ฆผ 20% 150 203.0 771.5 219.0 -7.9% 80% 94.0% 89.3% 0\n",
" ํ์ ์ ๊ฐ 59.5% (๋ชฉํ 15% ์ด์: ๋ฌ์ฑ), 15% ์ด์ ๋ฌ์ฑ ์๋๋ฆฌ์ค: ํ์(์ ์ ์กฐ๋ช
) 59.5%, ์ ์กฐ๋ ์ฌ์ง ๋ง์ 28.2%\n",
" AI ๋จ๋
๋น์ฉ์ ํ์ 379, ์งํ 772๋ก ์ค์ผ ์ฌ์ง์์ ๊ณผ์ ํ์ ์์ค์ด ํญ์ฆํ๋ฏ๋ก ํ์ฉํ์ง ์๋๋ค(๋ชจ๋ ํ์ ์ ์ฌ๋์ด ์น์ธ). ์งํ 20%๋ ์ ๋ฐ ํ๋
์ด ๋์ด ์ ๊ฐ์ด ๋์ง ์๋๋ค(๋ฏธ๋ฌ).\n",
"\n",
"[2] ์ ๋ขฐ๋ ๊ธฐ์ค T ๋ฏผ๊ฐ๋(ํ์ 300์ฅ)\n",
" ๊ธฐ์ค T ๋น์ฉ ์ ๋ฐ ํ๋
๋น์จ ์ผ์น์จ ๊ณผ์ ํ์ \n",
" 60 267.20 0.027 0.967 8\n",
" 70 158.75 0.117 0.990 2\n",
" 80 178.90 0.220 0.990 2\n",
" 90 238.00 0.400 0.977 2\n",
" 95 238.00 0.400 0.977 2\n",
" ๋น์ฉ ์ต์ ๊ธฐ์ค T=70. ๊ธฐ์ค์ ๋๋ฌด ๋์ด๋ฉด ์ ๋ฐ ํ๋
์ด ๋์ด ๋น์ฉ์ด ์ค๋ฅด๊ณ , ๋๋ฌด ๋ฎ์ผ๋ฉด AI ์คํ์ด ํต๊ณผํ๋ค.\n",
" ์งํ 20% ์๋๋ฆฌ์ค, ๊ฒ์ฌ๊ด ํ๋ 150์ฅ/์ผ: ๋น์ฉ 210 (๊ดํ ๋๋น ์ ๊ฐ -3.7%), ์ ๋ฐ ํ๋
100%, ์ผ์น์จ 94.0%, ๊ณผ์ ํ์ 1์ฅ\n",
" ์งํ 20% ์๋๋ฆฌ์ค, ๊ฒ์ฌ๊ด ํ๋ 60์ฅ/์ผ: ๋น์ฉ 300 (๊ดํ ๋๋น ์ ๊ฐ -47.8%), ์ ๋ฐ ํ๋
40%, ์ผ์น์จ 81.3%, ๊ณผ์ ํ์ 3์ฅ\n",
" ์งํ 20% ์๋๋ฆฌ์ค, ๊ฒ์ฌ๊ด ํ๋ 20์ฅ/์ผ: ๋น์ฉ 396 (๊ดํ ๋๋น ์ ๊ฐ -95.1%), ์ ๋ฐ ํ๋
13%, ์ผ์น์จ 75.3%, ๊ณผ์ ํ์ 7์ฅ\n",
" ๊ฒ์ฌ๊ด ํ๋๊ฐ ์ค๋ฉด ์ ๋ขฐ๋ ๋ฎ์ ์ฌ์ง๊น์ง ๊ฐ์ด ์น์ธ์ผ๋ก ๋์ด๊ฐ AI ์คํ์ด ๊ฑธ๋ฌ์ง์ง ์์ ์์ค์ด ์ปค์ง๋ค(๋น์ฉ +). ์ค์ผ ์ฌ์ง์ ์ฌ์ดฌ์ยท์ฌ์ ์ธ์ฒ ์๋ด๋ก ์ค์ฌ์ผ ํ๋ค.\n",
"\n",
"[3] AI ํ์ 8๊ฑด ์ ๋ถ '๊ฒ์ฌ๋๊ธฐ' ๋ฑ๋ก, ์๋ ํ์ 0๊ฑด (๋ชฉํ 0๊ฑด)\n",
" ๊ถํ ์ฐํ ์๋ 10๊ฑด ์ค ์ฐจ๋จ 10๊ฑด (์ฐจ๋จ์จ 100%, ๋ชฉํ 100%)\n",
" ์ฐจ๋จ ai_bot review INS08: AI ์ญํ ์ review ๋ถ๊ฐ\n",
" ์ฐจ๋จ sup_park approve INS08: ๊ฒฐ์ฌ ์์ ์๋ฐ\n",
" ์ฐจ๋จ aud_han approve INS08: AUD ์ญํ ์ approve ๋ถ๊ฐ\n",
" ์ฐจ๋จ ghost review INS08: ๋ฏธ๋ฑ๋ก ์ฌ์ฉ์\n",
" ๊ฐ์ฌ ๋ก๊ทธ 25๊ฑด ํด์ ์ฒด์ธ ๋ฌด๊ฒฐ์ฑ 100%; 4๋ฒ ๊ธฐ๋ก ์ฌํ ์์ ์ ์๋ณ์กฐ ๊ฒ์ถ(4๋ฒ)\n",
" INS04: AI ์ผ์ ์ ๋น -> ๊ฒ์ฌ๊ด ๋ณด์ ์ฐฝ์ ๋น(๋ณด์ ์ ํ์ง๋ณด์ฆ๊ด๊น์ง 3๋จ๊ณ ๊ฒฐ์ฌ, ์ํ ํ์ ); INS03(๋ฐ๋ ค) -> INS09(์ฌ๋ฑ๋ก, parent ์ฐ๊ฒฐ)\n",
" ๋ฌธ์ ์ํ: {'๊ฒ์ฌ๋๊ธฐ': 6, 'ํ์ ': 2, '๋ฐ๋ ค': 1} | ์ต์ข
์ ๋น ๋ฑ๊ธ์ ๊ฒ์ฌ๊ดยท์ ๋น๊ฐ๋
๊ด์ ๊ฒฐ์ฌ๋ก ํ์ ๋๋ฉฐ AI๋ ํ์ ๊ถํ์ด ์๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
08. ์ค์ผ๋ฌผ/์งํ ํผ์
์ฌ์ง ๋๋ ํ์ ์คํธ๋ ์ค ํ
์คํธ ๋ฐ ํ๊ณ์น ๋์ถ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ค์ผ๋ฌผ ํผ์
์ฌ์ง ๋๋ ํ์ ์คํธ๋ ์ค ํ
์คํธ๋ฅผ ์ํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ค์ผ๋ฌผยท์งํ์ด ์์ธ ์ฌ์ง์ ๋๋ ํ์ ํ๋ ์คํธ๋ ์ค ํ
์คํธ๋ฅผ ์ํํ๊ณ ํ์ ํ๊ณ์น๋ฅผ ๋์ถํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์งํ ํผ์
๋น์จ์ 0~50%๋ก ์ฌ๋ฆฐ ์ฌ์ง ๋ฌถ์๊ณผ ํ๋ฆผยท์ผ์ ์ก์ยท์ ์กฐ๋+์งํ ๊ฒฐํฉ ์กฐ๊ฑด์์ ๋ฑ๊ธ ์ผ์น์จ, ์ฌ์ดฌ์ ์๊ตฌ์จ, ์๋ ์ฒ๋ฆฌ ๋น์จ, ๊ฒฝ๋ณด ์ ํ๋๋ฅผ ์ธก์ ํ๊ณ ํ์ง ๊ฒ์ดํธ์ ํจ๊ณผ๋ฅผ ๋น๊ต ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/dashboard/qlt03_app.py (Streamlit/Dash ๋ฑ)` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์์ฌ๊ฒฐ์ ์ง์์ ์ต์ ํ๋ ์๊ฐ์ ์๊ณ ๋ฐ ์ฌ์ฉ์ ๊ฒฝํ(UX)\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ๋์๋ณด๋ ๋ก๋ฉ ์๊ฐ 2์ด ์ด๋ด, ๊ฒฝ๋ณด ๋ฐ๋ น ์ ํ๋ 95% ์ด์\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์น ๊ธฐ๋ฐ ๋ถ์ ๋์๋ณด๋ ๊ตฌ๋ ๋ฐ ์ค์๊ฐ ํํฐ๋ง ๊ธฐ๋ฅ ๊ฒ์ฆ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ์ค์ ๊ตฐ์ ๋ฐ์ดํฐ ์ฐ๋ ํ๋ฉด ํ
์คํธ ๋ฐ UI ๋์ ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์งํยท์ค์ผ๋ฌผ ํผ์
๋น์จ์ 0~50%๋ก ์ฌ๋ฆฐ ์ฌ์ง ์๋ฐฑ ์ฅ๊ณผ ํ๋ฆผยท์ผ์ ์ก์ยท์ ์กฐ๋ ๊ฒฐํฉ ์ฌ์ง์ ์ผ๊ด ํ์ ํ๋ ์คํธ๋ ์ค ํ
์คํธ๋ก ์ค์ผ ์ฌ์ง ํ์ ํ๊ณ์น(์๋ ์ฒ๋ฆฌ ๊ฐ๋ฅ ๊ฐ๋ฆผ ๋น์จ)์ ๊ฒฝ๋ณด ์ ํ๋, ๋๋ ์ฒ๋ฆฌ ์๊ฐยท๋์๋ณด๋ ๋ก๋ฉ ์๊ฐ์ ์ธก์ ํ๊ณ ํ์ง ๊ฒ์ดํธ(์ฌ์ดฌ์ยท๊ฒ์ฌ๊ด ํ์ธ)์ ํจ๊ณผ๋ฅผ ํ์ธํ๋ค.\"\"\"\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.ensemble import RandomForestClassifier, GradientBoostingRegressor\n",
"from sklearn.model_selection import cross_val_predict\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ฅ๋น ์ธ๊ด ์ฌ์ง ์์ฑ๊ธฐ (์ค์ ์ฅ๋นยท๋ถ๋ยท์์น ์ฌ์ง์ด ์๋๋ผ ๋์๋ก ๊ทธ๋ฆฐ 64x64 ํฉ์ฑ ์ด๋ฏธ์ง)\n",
"# ๋์ฅ๋ ๊ธ์ ํ๋ฉด์ ๊ท ์ด(crack)ยท๋ถ์(corrosion)ยทํ์(damage)ยท๋์ (leak)๋ฅผ ๊ทธ๋ฆฌ๊ณ , ์กฐ๋ช
ํธ์ฐจ์ ์งํ(mud) ์ค์ผ์ ์๋๋ค.\n",
"# ์ด๋ฏธ์ง์ ํจ๊ป ์ฅ๋น ๊ด๋ฆฌ ๊ธฐ๋ก(์ ์ ์์ฝ)๊ณผ ์ ๋ต ๊ฒฐํจ ๋ง์คํฌยท์์๋ยท์ ๋น ๋ฑ๊ธ์ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"from scipy import ndimage as ndi\n",
"\n",
"SZ = 64\n",
"CLASSES = [\"none\", \"crack\", \"corrosion\", \"damage\", \"leak\"]\n",
"KO_CLS = {\"none\": \"๊ฒฐํจ ์์\", \"crack\": \"๊ท ์ด\", \"corrosion\": \"๋ถ์\", \"damage\": \"ํ์\", \"leak\": \"๋์ \"}\n",
"CLS_W = {\"none\": 0.0, \"crack\": 1.4, \"corrosion\": 1.0, \"damage\": 1.6, \"leak\": 0.6} # ์์๋ ๊ณ์ฐ์ฉ ๊ฒฐํจ ์ ํ ๊ฐ์ค์น\n",
"GRADE_NAME = {1: \"ํ์ฅ์ ๋น\", 2: \"์ผ์ ์ ๋น\", 3: \"์ฐฝ์ ๋น\"}\n",
"YY, XX = np.mgrid[0:SZ, 0:SZ]\n",
"\n",
"\n",
"def grade_of(idx):\n",
" # [TODO : ํ์ ์ค์ต 8] ์ค์ผ๋ฌผ/์งํ ํผ์
์ฌ์ง ๋๋ ํ์ ์คํธ๋ ์ค ํ
์คํธ ๋ฐ ํ๊ณ์น ๋์ถ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def blob(rng, cy, cx, r, rough=0.35):\n",
" \"\"\"๋ถ๊ท์นํ ์ํ ์์ญ ๋ง์คํฌ.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 8] ์ค์ผ๋ฌผ/์งํ ํผ์
์ฌ์ง ๋๋ ํ์ ์คํธ๋ ์ค ํ
์คํธ ๋ฐ ํ๊ณ์น ๋์ถ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def draw(rng, cls, sev):\n",
" \"\"\"๊ฒฐํจ ์ ํยท์ฌ๊ฐ๋ sev(0~1)์ ๋ง๋ ์ ๋ต ๋ง์คํฌ์ ์ ์ ๋ณด๋ฅผ ๋ง๋ ๋ค.\"\"\"\n",
" m = np.zeros((SZ, SZ), bool)\n",
" if cls == \"crack\":\n",
" y, x, ang = rng.uniform(12, 52), rng.uniform(12, 52), rng.uniform(0, np.pi)\n",
" for _ in range(int(20 + 40 * sev)):\n",
" ang += rng.normal(0, 0.35); y += np.sin(ang) * 1.2; x += np.cos(ang) * 1.2\n",
" if 0 <= int(y) < SZ and 0 <= int(x) < SZ:\n",
" m[int(y), int(x)] = True\n",
" m = ndi.binary_dilation(m, iterations=1 + int(sev > 0.6))\n",
" elif cls == \"corrosion\":\n",
" for _ in range(int(rng.integers(1, 4))):\n",
" m |= blob(rng, rng.uniform(14, 50), rng.uniform(14, 50), 3 + 9 * sev * rng.uniform(0.6, 1.1))\n",
" elif cls == \"damage\":\n",
" m = blob(rng, rng.uniform(20, 44), rng.uniform(20, 44), 6 + 12 * sev, 0.3)\n",
" elif cls == \"leak\":\n",
" x0, y0 = rng.uniform(16, 48), rng.uniform(8, 24)\n",
" w, ln = 2 + 4 * sev, 12 + 26 * sev\n",
" m = (np.abs(XX - x0 - 0.15 * (YY - y0)) < w * (1 - 0.4 * (YY - y0) / ln)) & (YY > y0) & (YY < y0 + ln)\n",
" return m\n",
"\n",
"\n",
"def make_images(n=600, seed=0, mud=0.0, p_none=0.25, dark=False):\n",
" \"\"\"๊ฐ์ ์ธ๊ด ์ฌ์ง n์ฅ(+์งํ ์ค์ผ mud ๋น์จ)๊ณผ ๊ด๋ฆฌ ๊ธฐ๋กยท์ ๋ต. ๋ฐํ: meta(DataFrame), imgs(n,64,64,3), masks(n,64,64).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" imgs, masks, rows = np.zeros((n, SZ, SZ, 3), np.float32), np.zeros((n, SZ, SZ), bool), []\n",
" for i in range(n):\n",
" cls = \"none\" if rng.random() < p_none else str(rng.choice(CLASSES[1:], p=[0.25, 0.3, 0.25, 0.2]))\n",
" sev = 0.0 if cls == \"none\" else float(rng.beta(1.6, 2.0))\n",
" base = np.array([0.42, 0.46, 0.40]) * (1 + 0.5 * ndi.gaussian_filter(rng.normal(size=(SZ, SZ)), 8)[..., None] * 3)\n",
" img = base + rng.normal(0, 0.012, (SZ, SZ, 3))\n",
" m = draw(rng, cls, sev)\n",
" a = ndi.gaussian_filter(m.astype(float), 0.8)[..., None] # ๊ฒฝ๊ณ๋ฅผ ๋ถ๋๋ฝ๊ฒ ์๋๋ค\n",
" if cls == \"crack\":\n",
" col = img * rng.uniform(0.2, 0.45)\n",
" elif cls == \"corrosion\":\n",
" col = np.array([0.55, 0.30, 0.12]) * rng.uniform(0.75, 1.15) * (0.85 + 0.3 * rng.random((SZ, SZ, 1)))\n",
" elif cls == \"damage\":\n",
" col = np.ones_like(img) * np.array([0.34, 0.34, 0.36]) * (1.0 - 0.45 * sev) * rng.uniform(0.75, 1.0)\n",
" rim = m & ~ndi.binary_erosion(m, iterations=2)\n",
" a = np.maximum(a, 0.0); img = np.where(rim[..., None], 0.7, img)\n",
" elif cls == \"leak\":\n",
" col = np.ones_like(img) * np.array([0.10, 0.09, 0.08]) * rng.uniform(0.8, 2.6)\n",
" else:\n",
" col = img\n",
" img = img * (1 - a) + col * a\n",
" if rng.random() < 0.35: # ๋ฌดํดํ ์ผ๋ฃฉ(๊ฒฐํจ ์๋): ์
์ ๊ฐ์ ๋ฒ์ง\n",
" st = blob(rng, rng.uniform(5, 58), rng.uniform(5, 58), rng.uniform(6, 14), 0.5)\n",
" al = ndi.gaussian_filter(st.astype(float), 2.5)[..., None] * rng.uniform(0.15, 0.4)\n",
" img = img * (1 - al) + np.array([0.50, 0.40, 0.30]) * al\n",
" if rng.random() < 0.3: # ๋ฌดํดํ ๊ธด ์คํฌ๋์น(๋ฐ์ ์ )\n",
" y0, x0, ang = rng.uniform(5, 58), rng.uniform(5, 58), rng.uniform(0, np.pi)\n",
" for t in np.linspace(0, 25, 40):\n",
" yy, xx = int(y0 + t * np.sin(ang)), int(x0 + t * np.cos(ang))\n",
" if 0 <= yy < SZ and 0 <= xx < SZ:\n",
" img[yy, xx] = np.clip(img[yy, xx] * 1.5, 0, 1)\n",
" if mud > 0: # ์งํ ์ค์ผ: ๊ฒฐํจ ์๋ฅผ ๋ฎ์ ์ ์๋ค(์ ๋ต ๋ง์คํฌ๋ ๊ทธ๋๋ก)\n",
" mm = np.zeros((SZ, SZ), bool)\n",
" while mm.mean() < mud:\n",
" mm |= blob(rng, rng.uniform(0, SZ), rng.uniform(0, SZ), rng.uniform(5, 11), 0.5)\n",
" img[mm] = np.array([0.38, 0.30, 0.22]) * (0.85 + 0.3 * rng.random((mm.sum(), 1)))\n",
" gain = rng.uniform(0.3, 0.55) if dark else rng.uniform(0.5, 1.15)\n",
" ang = rng.uniform(0, 2 * np.pi)\n",
" light = gain * (1 + 0.3 * (np.cos(ang) * (XX - 32) + np.sin(ang) * (YY - 32)) / 32)\n",
" imgs[i] = np.clip(img * light[..., None] + rng.normal(0, 0.025, (SZ, SZ, 3)), 0, 1)\n",
" masks[i] = m\n",
" area = float(m.mean())\n",
" depth = {\"none\": 0.0, \"crack\": 0.3 + 3.0 * sev, \"corrosion\": 0.1 + 1.5 * sev, \"damage\": 1.0 + 8.0 * sev, \"leak\": 0.0}[cls] * rng.lognormal(0, 0.1)\n",
" idx = float(np.clip(100 * (1 - np.exp(-(CLS_W[cls] * area * 9 + 0.05 * depth))) + rng.normal(0, 1.5), 0, 100))\n",
" rows.append({\"img_id\": f\"IMG{seed:02d}{i:04d}\", \"cls\": cls, \"sev\": sev, \"area_frac\": area, \"depth_mm\": depth, \"damage_idx\": idx,\n",
" \"grade\": int(grade_of(idx)), \"gain\": gain, \"mud\": mud})\n",
" meta = pd.DataFrame(rows)\n",
" # ์ฅ๋น ๊ด๋ฆฌ ๊ธฐ๋ก(์ ์ ์์ฝ): ์ฌ์ง ํ์ ๊ณผ ๋์กฐํ ๊ฐ\n",
" age = rng.integers(1, 25, n)\n",
" meta[\"day\"] = np.sort(rng.integers(0, 90, n))\n",
" meta[\"equip_type\"] = rng.choice([\"vehicle\", \"generator\", \"comm\", \"trailer\"], n, p=[0.4, 0.2, 0.2, 0.2])\n",
" meta[\"mfg_year\"] = 2026 - age\n",
" meta[\"op_hours\"] = np.clip(rng.normal(180 * age, 400, n) + 8 * meta[\"damage_idx\"], 10, 20000)\n",
" meta[\"env\"] = rng.choice([\"coastal\", \"inland\"], n, p=[0.3, 0.7])\n",
" reg = np.clip(meta[\"grade\"] + rng.choice([-1, 0, 0, 0, 0, 0, 1, 2], n), 1, 3) # ์ฅ๋น ๊ด๋ฆฌ ๊ธฐ๋ก์ ์ง์ ์ ๋น ๋ฑ๊ธ\n",
" meta[\"reg_grade\"] = reg\n",
" meta[\"emergency\"] = rng.random(n) < 0.04 # ๊ธด๊ธ ์ ๊ฒ ์์ฒญ(ํ์ง ๋ถ๋์ด์ด๋ ๋ณด์กด)\n",
" return meta, imgs, masks\n",
"# ---- ์ ์ฒ๋ฆฌ(์กฐ๋ช
๋ณด์ ยทํ์คํ)์ ๊ฒฐํจ ํ๋ณด ๋ถํ , ํน์ง ์ถ์ถ (cv2 ๋์ numpyยทscipy ์ฌ์ฉ) ----\n",
"def correct(img):\n",
" \"\"\"์กฐ๋ช
๋ณด์ : ๋ฐ๊ธฐ์ ํ๋ฉด(์กฐ๋ช
๊ธฐ์ธ๊ธฐ)์ ์ ํฉํด ๋๋๋ค. ์ด๋์ด ๊ฒฐํจ ํ์๋ ์ ์ธํ๊ณ ๋ ๋ฒ ๋ฐ๋ณต ์ ํฉํ๋ค. ๋ฐํ: ๋ณด์ ๋ฐ๊ธฐ(์ ์ ๋ฉด ์ฝ 1), ์ ๋น์จ.\"\"\"\n",
" g = img.mean(axis=2)\n",
" A = np.c_[np.ones(SZ * SZ), XX.ravel() / SZ, YY.ravel() / SZ]\n",
" keep = np.ones(SZ * SZ, bool)\n",
" for _ in range(3):\n",
" coef, *_ = np.linalg.lstsq(A[keep], g.ravel()[keep], rcond=None)\n",
" plane = (A @ coef).reshape(SZ, SZ)\n",
" keep = (np.abs(g - plane) < 0.08 * np.maximum(plane, 1e-3)).ravel()\n",
" if keep.sum() < 200:\n",
" keep[:] = True\n",
" c = g / np.maximum(plane, 1e-3)\n",
" chroma = img[..., 0] / (img.sum(axis=2) + 1e-6)\n",
" return c, chroma\n",
"\n",
"\n",
"SEGP = {\"dark\": 0.7, \"rust\": 0.5, \"rim\": 1.45} # ๋ถํ ์๊ณ๊ฐ(์ด๋์ ๋ณด์ ๋ฐ๊ธฐ, ๋ถ์ ๋น์จ, ๋ฐ์ ํ
๋๋ฆฌ): ์ฌ๋ณด์ ๋์\n",
"\n",
"\n",
"def segment(img):\n",
" \"\"\"๊ฒฐํจ ํ๋ณด ๋ง์คํฌ: ์ด๋์ด ์์ญ(๊ท ์ดยท๋์ ยทํ์) + ๋ถ์ ๊ฐ์ ์์ญ(๋ถ์) + ๋ฐ์ ํ
๋๋ฆฌ(ํ์).\"\"\"\n",
" c, chroma = correct(img)\n",
" dark = c < SEGP[\"dark\"]\n",
" rust = (chroma > SEGP[\"rust\"]) & (c > 0.45)\n",
" rim = c > SEGP[\"rim\"]\n",
" m = dark | rust | ndi.binary_dilation(rim, iterations=1) & ndi.binary_dilation(dark, iterations=3)\n",
" m = ndi.binary_opening(m, structure=np.ones((2, 2))) if m.sum() > 12 else m\n",
" return m, c, chroma, dark, rust, rim\n",
"\n",
"\n",
"def extract(img):\n",
" \"\"\"๋ถํ ๊ฒฐ๊ณผ์ ์ยท๋ชจ์์์ 24๊ฐ ํน์ง์ ๋ง๋ ๋ค.\"\"\"\n",
" m, c, chroma, dark, rust, rim = segment(img)\n",
" lab, n = ndi.label(m)\n",
" area = float(m.sum())\n",
" if n:\n",
" sizes = ndi.sum(m, lab, range(1, n + 1))\n",
" k = int(np.argmax(sizes)) + 1\n",
" sl = ndi.find_objects(lab)[k - 1]\n",
" h, w = sl[0].stop - sl[0].start, sl[1].stop - sl[1].start\n",
" big = float(sizes.max())\n",
" elong, fill = max(h, w) / max(min(h, w), 1), big / (h * w)\n",
" else:\n",
" big, elong, fill = 0.0, 1.0, 0.0\n",
" gy, gx = np.gradient(c)\n",
" mag = np.hypot(gx, gy)\n",
" inside = m.sum() > 0\n",
" return {\"area_px\": area, \"area_frac\": area / SZ ** 2, \"n_comp\": float(n), \"big_px\": big, \"elong\": elong, \"fill\": fill,\n",
" \"dark_frac\": float(dark.mean()), \"rust_frac\": float(rust.mean()), \"rim_frac\": float(rim.mean()),\n",
" \"c_mean\": float(c.mean()), \"c_std\": float(c.std()), \"c_min\": float(c.min()), \"edge_mean\": float(mag.mean()), \"edge_p95\": float(np.quantile(mag, 0.95)),\n",
" \"chroma_mean\": float(chroma.mean()), \"chroma_max\": float(chroma.max()),\n",
" \"in_dark\": float(c[m].mean()) if inside else 1.0, \"in_chroma\": float(chroma[m].mean()) if inside else float(chroma.mean()),\n",
" \"ring_contrast\": float(c[ndi.binary_dilation(m, iterations=3) & ~m].mean() - c[m].mean()) if inside and (~m).any() else 0.0,\n",
" \"thin\": float(1 - fill), \"gain\": float(img.mean()), \"sat\": float((img.max(axis=2) - img.min(axis=2)).mean()),\n",
" \"cy\": float(ndi.center_of_mass(m)[0] / SZ) if inside else 0.5, \"cx\": float(ndi.center_of_mass(m)[1] / SZ) if inside else 0.5}\n",
"\n",
"\n",
"def feature_table(imgs):\n",
" return pd.DataFrame([extract(im) for im in imgs])\n",
"# ---- ํ์ ํ์ดํ๋ผ์ธ: ๊ฒฐํจ ์ ํ ๋ถ๋ฅ + ์์๋ ํ๊ท(๋ถ๋ฅ ํ๋ฅ ํฌํจ) -> ์ ๋น ๋ฑ๊ธ ----\n",
"def fit_pipeline(meta, F):\n",
" \"\"\"๊ฒฐํจ ์ ํ ๋ถ๋ฅ๊ธฐ์ ์์๋ ํ๊ท๊ธฐ๋ฅผ ํ์ตํ๋ค. ํ๊ท๊ธฐ ์
๋ ฅ์ ๋ถ๋ฅ ํ๋ฅ ์ ๊ต์ฐจ ์์ธก์ผ๋ก ๋ง๋ค์ด ๋์ค์ ๋ง๋๋ค.\"\"\"\n",
" clf = RandomForestClassifier(n_estimators=150, random_state=0, n_jobs=1)\n",
" P = cross_val_predict(clf, F, meta[\"cls\"], cv=4, method=\"predict_proba\")\n",
" clf.fit(F, meta[\"cls\"])\n",
" reg = GradientBoostingRegressor(n_estimators=200, max_depth=3, learning_rate=0.06, random_state=0).fit(np.c_[F.values, P], meta[\"damage_idx\"])\n",
" return {\"clf\": clf, \"reg\": reg, \"cols\": list(F.columns)}\n",
"\n",
"\n",
"def predict_pipeline(pl, F):\n",
" X = F[pl[\"cols\"]]\n",
" P = pl[\"clf\"].predict_proba(X)\n",
" idx = np.clip(pl[\"reg\"].predict(np.c_[X.values, P]), 0, 100)\n",
" return pl[\"clf\"].classes_[P.argmax(axis=1)], P, idx, P.max(axis=1)\n",
"# ---- ์ฌ์ง ํ์ง ๊ฒ์ดํธ: ์งํยท์ค์ผ ๊ฐ๋ฆผ ์ถ์ ๊ณผ ํ์ ์ ๋ขฐ๋ ์ง์ ----\n",
"def mud_est(img):\n",
" \"\"\"์งํ(๊ฐ์, ์ฑ๋ ๋ฎ์ ๋ถ์ ๊ธฐ) ํ์ ๋น์จ์ ์ถ์ ํ๋ค. 3x3 ํ๊ท ์ผ๋ก ์ก์์ ์ค์ธ ๋ค ์ ๋น์จ๋ก ํ๋จํ๋ฉฐ, ์ค์ ๊ฐ๋ฆผ ๋น์จ๋ณด๋ค ์๊ฒ ๋์ค์ง๋ง ์์๋ ์ ์ง๋๋ค.\"\"\"\n",
" sm = ndi.uniform_filter(img, size=(3, 3, 1))\n",
" s = sm.sum(axis=2) + 1e-6\n",
" rr, gg = sm[..., 0] / s, sm[..., 1] / s\n",
" return float(((rr > 0.385) & (rr < 0.46) & (gg < 0.325)).mean())\n",
"\n",
"\n",
"def confidence(P, idx, F, mud):\n",
" \"\"\"ํ์ ์ ๋ขฐ๋ ์ง์ 0~100 = 50 x ๋ถ๋ฅ ํ๋ฅ ์ต๋๊ฐ + 35 x ๋ฑ๊ธ ๊ฒฝ๊ณ๊น์ง ๊ฑฐ๋ฆฌ(8์ ์์ ํฌํ) + 15 x ์ฌ์ง ํ์ง. ์ค์ผ ์์ฌ(mud ์ถ์ 1% ์ด์)์ด๋ฉด ์ํ 75๋ก ์ ํํ๋ค.\"\"\"\n",
" bd = np.minimum(np.abs(idx - 35), np.abs(idx - 65))\n",
" qual = np.clip(1.0 - np.clip(0.55 - F[\"gain\"].values, 0, None) * 2 - np.clip(0.9 - F[\"c_mean\"].values, 0, None), 0, 1)\n",
" c = 50 * P.max(axis=1) + 35 * np.clip(bd / 8, 0, 1) + 15 * qual\n",
" return np.where(np.asarray(mud) >= 0.01, np.minimum(c, 75.0), c)\n",
"\n",
"mtr, itr, _ = make_images(600, seed=1)\n",
"pl = fit_pipeline(mtr, feature_table(itr))\n",
"\n",
"\n",
"def run(imgs, truth):\n",
" \"\"\"์ฌ์ง ๋ฌถ์์ ํ์ ํด ๊ฒ์ดํธ ์ ์ฉ ์ ยทํ ์ ๋ขฐ๋์ ๊ฒฝ๋ณด(๋ฑ๊ธ 2 ์ด์ = ์ ๋น ๊ฒฝ๋ณด)๋ฅผ ๋ง๋ ๋ค.\"\"\"\n",
" t0 = time.perf_counter()\n",
" F = feature_table(imgs)\n",
" t1 = time.perf_counter()\n",
" cls, P, idx, _ = predict_pipeline(pl, F)\n",
" t2 = time.perf_counter()\n",
" mud = np.array([mud_est(x) for x in imgs])\n",
" raw_conf = confidence(P, idx, F, np.zeros(len(imgs)))\n",
" conf = confidence(P, idx, F, mud)\n",
" t3 = time.perf_counter()\n",
" g = grade_of(idx)\n",
" return pd.DataFrame({\"true\": truth[\"grade\"].values, \"ai\": g, \"mud\": mud, \"raw_conf\": raw_conf, \"conf\": conf, \"idx\": idx, \"retake\": (mud >= 0.03) | (F[\"gain\"].values < 0.17)}), (t1 - t0, t2 - t1, t3 - t2)\n",
"\n",
"\n",
"# ---------------- 1) ์งํ ํผ์
๋น์จ ์ค์ ----------------\n",
"rows, tm = [], []\n",
"for mudr in (0.0, 0.05, 0.10, 0.20, 0.30, 0.40, 0.50):\n",
" m, im, _ = make_images(120, seed=20 + int(mudr * 100), mud=mudr)\n",
" r, t = run(im, m)\n",
" tm.append(t)\n",
" ok = r[\"true\"] == r[\"ai\"]\n",
" auto = (r[\"conf\"] >= 80) & ~r[\"retake\"] # ํ์ง ๊ฒ์ดํธ๋ฅผ ํต๊ณผํด ์๋ ์ฒ๋ฆฌ(๊ฒ์ฌ๊ด ๊ฐ์ด ์น์ธ)๋ก ๊ฐ๋ ์ฌ์ง\n",
" alarm_ok = ((r[\"ai\"] >= 2) == (r[\"true\"] >= 2))\n",
" ung = r[\"raw_conf\"] >= 80\n",
" rows.append((f\"{mudr:.0%}\", len(r), ok.mean(), r[\"conf\"].mean(), r[\"retake\"].mean(), auto.mean(), alarm_ok[auto].mean() if auto.any() else np.nan, alarm_ok[ung].mean() if ung.any() else np.nan, ung.mean()))\n",
"sw = pd.DataFrame(rows, columns=[\"์งํ ๋น์จ\", \"์ฅ์\", \"๋ฑ๊ธ ์ผ์น์จ(์ ์ฒด)\", \"ํ๊ท ์ ๋ขฐ๋\", \"์ฌ์ดฌ์ ์๊ตฌ์จ\", \"์๋ ์ฒ๋ฆฌ ๋น์จ\", \"๊ฒฝ๋ณด ์ ํ๋(๊ฒ์ดํธ ํ)\", \"๊ฒฝ๋ณด ์ ํ๋(๊ฒ์ดํธ ์์ด 80 ์ด์)\", \"๊ฒ์ดํธ ์์ด 80 ์ด์ ๋น์จ\"])\n",
"print(\"[1] ์งํ ํผ์
๋น์จ๋ณ ํ์ (๊ต์ก์ฉ ๊ฐ์ ์ฌ์ง, ๋น์จ๋น 120์ฅ)\")\n",
"pc = lambda v: \"-\" if pd.isna(v) else f\"{v:.0%}\"\n",
"print(sw.assign(**{c: sw[c].map(pc) for c in sw.columns if c not in (\"์งํ ๋น์จ\", \"์ฅ์\", \"ํ๊ท ์ ๋ขฐ๋\")}).round(1).to_string(index=False))\n",
"bad = sw[sw[\"๊ฒฝ๋ณด ์ ํ๋(๊ฒ์ดํธ ํ)\"] < .95][\"์งํ ๋น์จ\"].tolist()\n",
"print(f\" ๊ฒฝ๋ณด ์ ํ๋๊ฐ 95% ๋ฏธ๋ง์ผ๋ก ์ฒ์ ๋จ์ด์ง๋ ์งํ ๋น์จ: {bad[0] if bad else '์์'} (์งํ 0%์์๋ ์๋ ์ฒ๋ฆฌ {sw.iloc[0]['์๋ ์ฒ๋ฆฌ ๋น์จ']:.0%}, ๊ฒฝ๋ณด ์ ํ๋ {sw.iloc[0]['๊ฒฝ๋ณด ์ ํ๋(๊ฒ์ดํธ ํ)']:.0%})\")\n",
"print(f\" ๊ฒ์ดํธ๊ฐ ์์ผ๋ฉด ์งํ 20%์์๋ ์ ๋ขฐ๋ 80 ์ด์์ด {sw.iloc[3]['๊ฒ์ดํธ ์์ด 80 ์ด์ ๋น์จ']:.0%} ๋จ๊ณ ๊ทธ ๊ฒฝ๋ณด ์ ํ๋๋ {sw.iloc[3]['๊ฒฝ๋ณด ์ ํ๋(๊ฒ์ดํธ ์์ด 80 ์ด์)']:.0%}๋ก ๋ฎ๋ค. ํ์ง ๊ฒ์ดํธ๋ ์ด๋ฐ ์ฌ์ง์ ์๋ ์ฒ๋ฆฌ์์ ๋นผ ๊ฒ์ฌ๊ด ํ์ธ์ผ๋ก ๋ณด๋ธ๋ค.\")\n",
"\n",
"# ---------------- 2) ๋ณตํฉ ์ดํ: ํ๋ฆผ, ์ผ์ ์ก์, ์ ์กฐ๋+์งํ ----------------\n",
"rng = np.random.default_rng(3)\n",
"cases = {}\n",
"m0, i0, _ = make_images(120, seed=40)\n",
"cases[\"์ ์\"] = (m0, i0)\n",
"cases[\"ํ๋ฆผ(์ด์ ๋ถ๋)\"] = (m0, np.clip(np.stack([ndi.gaussian_filter(x, sigma=(1.8, 1.8, 0)) for x in i0]), 0, 1))\n",
"cases[\"์ผ์ ์ก์ +0.08\"] = (m0, np.clip(i0 + rng.normal(0, 0.08, i0.shape), 0, 1).astype(np.float32))\n",
"m1, i1, _ = make_images(120, seed=41, mud=0.2, dark=True)\n",
"cases[\"์ ์กฐ๋+์งํ 20%\"] = (m1, i1)\n",
"rows = []\n",
"for name, (m, im) in cases.items():\n",
" r, _ = run(im, m)\n",
" ok = r[\"true\"] == r[\"ai\"]\n",
" auto = (r[\"conf\"] >= 80) & ~r[\"retake\"]\n",
" am = ((r[\"ai\"] >= 2) == (r[\"true\"] >= 2))\n",
" rows.append((name, ok.mean(), r[\"retake\"].mean(), auto.mean(), am[auto].mean() if auto.any() else np.nan, (r[\"true\"] > r[\"ai\"]).mean()))\n",
"cc = pd.DataFrame(rows, columns=[\"์กฐ๊ฑด\", \"๋ฑ๊ธ ์ผ์น์จ\", \"์ฌ์ดฌ์ ์๊ตฌ์จ\", \"์๋ ์ฒ๋ฆฌ ๋น์จ\", \"์๋ ์ฒ๋ฆฌ ๊ฒฝ๋ณด ์ ํ๋\", \"๊ณผ์ ํ์ ๋น์จ\"])\n",
"print(\"\\n[2] ๋ณตํฉ ์ดํ ์กฐ๊ฑด(120์ฅ)\\n\" + cc.assign(**{c: cc[c].map(pc) for c in cc.columns[1:]}).to_string(index=False))\n",
"print(f\" ํ๋ฆผ ์ฌ์ง์ ๊ฒ์ดํธ๊ฐ ์ง์ ์ก์ง ๋ชปํด ์๋ ์ฒ๋ฆฌ {cc.iloc[1]['์๋ ์ฒ๋ฆฌ ๋น์จ']:.0%} ์ค ๊ฒฝ๋ณด ์ ํ๋ {cc.iloc[1]['์๋ ์ฒ๋ฆฌ ๊ฒฝ๋ณด ์ ํ๋']:.0%}๋ก 95%์ ๋ชป ๋ฏธ์น๋ค -> ์ด์ ยท์ดฌ์ ๊ฑฐ๋ฆฌ ๊ฐ์ด๋ ๋ณด์์ด ํ์ํ๋ค.\")\n",
"\n",
"# ---------------- 3) ๋๋ ์ฒ๋ฆฌ ์๊ฐ๊ณผ ๋์๋ณด๋ ๋ก๋ฉ ----------------\n",
"tt = np.array(tm).mean(axis=0) / 120\n",
"per = tt.sum()\n",
"print(f\"\\n[3] ์ฌ์ง 1์ฅ๋น ์ฒ๋ฆฌ ์๊ฐ(1๊ฐ ์ฝ์ด ๊ฐ์ ): ๋ถํ ยทํน์ง {tt[0] * 1000:.1f}ms, ํ์ {tt[1] * 1000:.2f}ms, ์ค์ผ ์ถ์ ยท์ ๋ขฐ๋ {tt[2] * 1000:.1f}ms, ํฉ๊ณ {per * 1000:.1f}ms ({1 / per:.0f}์ฅ/์ด)\")\n",
"for n in (1000, 5000, 20000):\n",
" print(f\" {n:>6}์ฅ ์ผ๊ด ํ์ ์์: {n * per:.0f}์ด ({n * per / 60:.1f}๋ถ) - ์ ํ ์ธ์ฝ, ์ฌ์ง ํด์๋ยท์ฅ๋น ์ฌ์์ด ๋ค๋ฅด๋ฉด ๋ฌ๋ผ์ง๋ค\")\n",
"\n",
"\n",
"def dashboard(df):\n",
" t0 = time.perf_counter()\n",
" summ = df.groupby(\"ai\").agg(๊ฑด์=(\"idx\", \"size\"), ํ๊ท ์ ๋ขฐ๋=(\"conf\", \"mean\"))\n",
" low = df.sort_values(\"conf\").head(20)\n",
" fg, ax = plt.subplots(1, 2, figsize=(9, 3.5))\n",
" ax[0].bar(summ.index.astype(str), summ[\"๊ฑด์\"]); ax[1].hist(df[\"conf\"], bins=20)\n",
" fg.canvas.draw(); plt.close(fg)\n",
" return time.perf_counter() - t0, len(low)\n",
"\n",
"\n",
"base, _ = run(cases[\"์ ์\"][1], cases[\"์ ์\"][0])\n",
"loads = []\n",
"for n in (300, 3000, 30000, 300000):\n",
" big = pd.concat([base] * (n // len(base) + 1), ignore_index=True).head(n)\n",
" loads.append((n, dashboard(big)[0]))\n",
"print(\" ๋์๋ณด๋ ๋ก๋ฉ ์๊ฐ(์์ฝ ์ง๊ณ + ์์ 20๊ฐ ๋ชฉ๋ก + ์ฐจํธ ๋ ๋๋ง):\", \", \".join(f\"{n}๊ฑด {s:.2f}์ด\" for n, s in loads))\n",
"print(f\" 3000๊ฑด ๊ธฐ์ค ๋ก๋ฉ {loads[1][1]:.2f}์ด(๋ชฉํ 2์ด ์ด๋ด: {'๋ฌ์ฑ' if loads[1][1] <= 2 else '๋ฏธ๋ฌ'}), 30๋ง ๊ฑด์์๋ {loads[3][1]:.2f}์ด. ํ๋ฉด์ ์์ฝ๊ณผ ์ ๋ขฐ๋ ๋ฎ์ ์ฌ์ง ์ฐ์ ๋ชฉ๋ก๋ง ๋จผ์ ํ์ํ๋ค.\")\n",
"\n",
"# ---------------- 4) ๊ฒฐ๋ก ----------------\n",
"print(\"\\n[4] ํ๊ณ์น ์์ฝ\")\n",
"print(f\" - ์๋ ์ฒ๋ฆฌ(๊ฒ์ฌ๊ด ๊ฐ์ด ์น์ธ) ํ๋ณด๋ ๊ฐ๋ฆผ์ด ์๋ ์ฌ์ง ์ค์ฌ์ด๋ฉฐ, ์งํ {bad[0] if bad else '5%'} ์ด์๋ถํฐ ๊ฒฝ๋ณด ์ ํ๋ 95% ๋ชฉํ๋ฅผ ์งํค์ง ๋ชปํด ์ฌ์ดฌ์ ๋๋ ๊ฒ์ฌ๊ด ์ ๋ฐ ํ๋
์ผ๋ก ๋ณด๋ธ๋ค\")\n",
"print(f\" - ์ ์ ์ฌ์ง๋ {1 - sw.iloc[0]['์๋ ์ฒ๋ฆฌ ๋น์จ']:.0%}๋ ์ ๋ขฐ๋ 80 ๋ฏธ๋ง์ด๋ผ ๊ฒ์ฌ๊ด ํ์ธ ๋์์ด๋ค. ๊ฒฝ๋ณด ์ ํ๋๋ ์๋ ์ฒ๋ฆฌ ์ฌ์ง์ ํํด ํ๋จํ๊ณ ์๋ ์ฒ๋ฆฌ ๋น์จ์ ํจ๊ป ๋ณด๊ณ ํ๋ค\")\n",
"print(\" - ํ์ ยท๊ฒฝ๋ณด๋ ๊ฒ์ฌ๊ด ํ์ธ์ ๋ณด์กฐ ์๋จ์ด๋ฉฐ ์ต์ข
์ ๋น ๋ฑ๊ธ์ ์ฌ๋์ด ํ์ ํ๋ค. ๋ชจ๋ ์ฌ์ง์ ๊ต์ก์ฉ ๊ฐ์ ์๋ฃ์ด๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(1, 3, figsize=(15, 4.2))\n",
"x = np.arange(len(sw))\n",
"ax[0].plot(x, sw[\"๋ฑ๊ธ ์ผ์น์จ(์ ์ฒด)\"] * 100, \"o-\", label=\"grade accuracy (all)\"); ax[0].plot(x, sw[\"์ฌ์ดฌ์ ์๊ตฌ์จ\"] * 100, \"s-\", label=\"retake rate\"); ax[0].plot(x, sw[\"์๋ ์ฒ๋ฆฌ ๋น์จ\"] * 100, \"^-\", label=\"auto-processed\")\n",
"ax[0].set_xticks(x); ax[0].set_xticklabels(sw[\"์งํ ๋น์จ\"]); ax[0].set_xlabel(\"mud ratio\"); ax[0].legend(); ax[0].set_title(\"Mud stress sweep (%)\")\n",
"ax[1].bar(range(len(cc)), cc[\"์๋ ์ฒ๋ฆฌ ๋น์จ\"] * 100); ax[1].set_xticks(range(len(cc))); ax[1].set_xticklabels([\"clean\", \"blur\", \"noise\", \"dark+mud\"]); ax[1].set_title(\"Auto-processed share (%)\")\n",
"ax[2].loglog([l[0] for l in loads], [l[1] for l in loads], \"o-\"); ax[2].axhline(2, c=\"r\", ls=\"--\"); ax[2].set_title(\"Dashboard load time (s) vs records\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
08 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[1] ์งํ ํผ์
๋น์จ๋ณ ํ์ (๊ต์ก์ฉ ๊ฐ์ ์ฌ์ง, ๋น์จ๋น 120์ฅ)\n",
"์งํ ๋น์จ ์ฅ์ ๋ฑ๊ธ ์ผ์น์จ(์ ์ฒด) ํ๊ท ์ ๋ขฐ๋ ์ฌ์ดฌ์ ์๊ตฌ์จ ์๋ ์ฒ๋ฆฌ ๋น์จ ๊ฒฝ๋ณด ์ ํ๋(๊ฒ์ดํธ ํ) ๊ฒฝ๋ณด ์ ํ๋(๊ฒ์ดํธ ์์ด 80 ์ด์) ๊ฒ์ดํธ ์์ด 80 ์ด์ ๋น์จ\n",
" 0% 120 90% 86.4 0% 73% 100% 100% 73%\n",
" 5% 120 65% 67.3 1% 12% 93% 94% 26%\n",
" 10% 120 53% 63.3 12% 3% 75% 88% 27%\n",
" 20% 120 42% 63.9 66% 0% - 73% 25%\n",
" 30% 120 64% 62.1 88% 0% - 77% 11%\n",
" 40% 120 59% 59.8 99% 0% - 38% 7%\n",
" 50% 120 68% 64.9 100% 0% - 79% 16%\n",
" ๊ฒฝ๋ณด ์ ํ๋๊ฐ 95% ๋ฏธ๋ง์ผ๋ก ์ฒ์ ๋จ์ด์ง๋ ์งํ ๋น์จ: 5% (์งํ 0%์์๋ ์๋ ์ฒ๋ฆฌ 73%, ๊ฒฝ๋ณด ์ ํ๋ 100%)\n",
" ๊ฒ์ดํธ๊ฐ ์์ผ๋ฉด ์งํ 20%์์๋ ์ ๋ขฐ๋ 80 ์ด์์ด 25% ๋จ๊ณ ๊ทธ ๊ฒฝ๋ณด ์ ํ๋๋ 73%๋ก ๋ฎ๋ค. ํ์ง ๊ฒ์ดํธ๋ ์ด๋ฐ ์ฌ์ง์ ์๋ ์ฒ๋ฆฌ์์ ๋นผ ๊ฒ์ฌ๊ด ํ์ธ์ผ๋ก ๋ณด๋ธ๋ค.\n",
"\n",
"[2] ๋ณตํฉ ์ดํ ์กฐ๊ฑด(120์ฅ)\n",
" ์กฐ๊ฑด ๋ฑ๊ธ ์ผ์น์จ ์ฌ์ดฌ์ ์๊ตฌ์จ ์๋ ์ฒ๋ฆฌ ๋น์จ ์๋ ์ฒ๋ฆฌ ๊ฒฝ๋ณด ์ ํ๋ ๊ณผ์ ํ์ ๋น์จ\n",
" ์ ์ 96% 0% 75% 100% 4%\n",
" ํ๋ฆผ(์ด์ ๋ถ๋) 84% 0% 62% 91% 11%\n",
"์ผ์ ์ก์ +0.08 66% 8% 8% 100% 8%\n",
" ์ ์กฐ๋+์งํ 20% 42% 100% 0% - 12%\n",
" ํ๋ฆผ ์ฌ์ง์ ๊ฒ์ดํธ๊ฐ ์ง์ ์ก์ง ๋ชปํด ์๋ ์ฒ๋ฆฌ 62% ์ค ๊ฒฝ๋ณด ์ ํ๋ 91%๋ก 95%์ ๋ชป ๋ฏธ์น๋ค -> ์ด์ ยท์ดฌ์ ๊ฑฐ๋ฆฌ ๊ฐ์ด๋ ๋ณด์์ด ํ์ํ๋ค.\n",
"\n",
"[3] ์ฌ์ง 1์ฅ๋น ์ฒ๋ฆฌ ์๊ฐ(1๊ฐ ์ฝ์ด ๊ฐ์ ): ๋ถํ ยทํน์ง 4.1ms, ํ์ 0.16ms, ์ค์ผ ์ถ์ ยท์ ๋ขฐ๋ 0.3ms, ํฉ๊ณ 4.5ms (223์ฅ/์ด)\n",
" 1000์ฅ ์ผ๊ด ํ์ ์์: 4์ด (0.1๋ถ) - ์ ํ ์ธ์ฝ, ์ฌ์ง ํด์๋ยท์ฅ๋น ์ฌ์์ด ๋ค๋ฅด๋ฉด ๋ฌ๋ผ์ง๋ค\n",
" 5000์ฅ ์ผ๊ด ํ์ ์์: 22์ด (0.4๋ถ) - ์ ํ ์ธ์ฝ, ์ฌ์ง ํด์๋ยท์ฅ๋น ์ฌ์์ด ๋ค๋ฅด๋ฉด ๋ฌ๋ผ์ง๋ค\n",
" 20000์ฅ ์ผ๊ด ํ์ ์์: 90์ด (1.5๋ถ) - ์ ํ ์ธ์ฝ, ์ฌ์ง ํด์๋ยท์ฅ๋น ์ฌ์์ด ๋ค๋ฅด๋ฉด ๋ฌ๋ผ์ง๋ค\n",
" ๋์๋ณด๋ ๋ก๋ฉ ์๊ฐ(์์ฝ ์ง๊ณ + ์์ 20๊ฐ ๋ชฉ๋ก + ์ฐจํธ ๋ ๋๋ง): 300๊ฑด 0.19์ด, 3000๊ฑด 0.10์ด, 30000๊ฑด 0.14์ด, 300000๊ฑด 0.17์ด\n",
" 3000๊ฑด ๊ธฐ์ค ๋ก๋ฉ 0.10์ด(๋ชฉํ 2์ด ์ด๋ด: ๋ฌ์ฑ), 30๋ง ๊ฑด์์๋ 0.17์ด. ํ๋ฉด์ ์์ฝ๊ณผ ์ ๋ขฐ๋ ๋ฎ์ ์ฌ์ง ์ฐ์ ๋ชฉ๋ก๋ง ๋จผ์ ํ์ํ๋ค.\n",
"\n",
"[4] ํ๊ณ์น ์์ฝ\n",
" - ์๋ ์ฒ๋ฆฌ(๊ฒ์ฌ๊ด ๊ฐ์ด ์น์ธ) ํ๋ณด๋ ๊ฐ๋ฆผ์ด ์๋ ์ฌ์ง ์ค์ฌ์ด๋ฉฐ, ์งํ 5% ์ด์๋ถํฐ ๊ฒฝ๋ณด ์ ํ๋ 95% ๋ชฉํ๋ฅผ ์งํค์ง ๋ชปํด ์ฌ์ดฌ์ ๋๋ ๊ฒ์ฌ๊ด ์ ๋ฐ ํ๋
์ผ๋ก ๋ณด๋ธ๋ค\n",
" - ์ ์ ์ฌ์ง๋ 27%๋ ์ ๋ขฐ๋ 80 ๋ฏธ๋ง์ด๋ผ ๊ฒ์ฌ๊ด ํ์ธ ๋์์ด๋ค. ๊ฒฝ๋ณด ์ ํ๋๋ ์๋ ์ฒ๋ฆฌ ์ฌ์ง์ ํํด ํ๋จํ๊ณ ์๋ ์ฒ๋ฆฌ ๋น์จ์ ํจ๊ป ๋ณด๊ณ ํ๋ค\n",
" - ํ์ ยท๊ฒฝ๋ณด๋ ๊ฒ์ฌ๊ด ํ์ธ์ ๋ณด์กฐ ์๋จ์ด๋ฉฐ ์ต์ข
์ ๋น ๋ฑ๊ธ์ ์ฌ๋์ด ํ์ ํ๋ค. ๋ชจ๋ ์ฌ์ง์ ๊ต์ก์ฉ ๊ฐ์ ์๋ฃ์ด๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
09. AAR ๊ธฐ๋ฐ ์ธ๊ด ์คํ ์ฌ๋ก ๋ถ์ ๋ฐ ์ปดํจํฐ ๋น์ ๋ชจ๋ธ ํ๋ผ๋ฏธํฐ ์ฌ๋ณด์ \n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"AAR ๊ธฐ๋ฐ ์ธ๊ด ์คํ ์ฌ๋ก๋ฅผ ๋ถ์ํ๊ณ ์ปดํจํฐ ๋น์ ํ๋ผ๋ฏธํฐ๋ฅผ ์ฌ๋ณด์ ํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **AAR๋ก ์ธ๊ด ์คํ ์ฌ๋ก๋ฅผ ๋ถ์ํ๊ณ ์ปดํจํฐ ๋น์ ๋ชจ๋ธ ํ๋ผ๋ฏธํฐ๋ฅผ ์ฌ๋ณด์ ํ๋ฉฐ ๋์
ํจ๊ณผ๋ฅผ ์ ๋ํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ด์ ๋ถํฌ(์ ์กฐ๋ยท์งํยท์ ์ ํผํฉ)์ ํ์ ๋ก๊ทธ์์ ์คํ์ ์ค์ผ ๊ฐ๋ฆผยท์ ์กฐ๋ยท์์ ๊ฒฐํจยท์ ํ ํผ๋ยท๊ฒฝ๊ณ ๊ทผ์ฒ๋ก ๋ถ๋ฅ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `reports/qlt03_roi_analysis.md` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์ถ์ ์ฐ์์ ๊ฐ๊ด์ฑ ๋ฐ ๊ตฐ ์ฌ์ /๊ตฐ์ ๊ธฐํ ๋ถ์ ์ค๋๋ ฅ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ๋น์ฉ ์ ๊ฐ์ก, ์๊ฐ ๋จ์ถ๋ฅ ๋ฑ 3๋ ์ ๋ ํจ๊ณผ ์งํ ์ฐ์ถ\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ROI ๋ถ์ ๋ณด๊ณ ์ ์์ฑ ๋ฐ ์ฃผ์ ์งํ ๋ฏผ๊ฐ๋ ๋ถ์ ์๋ก\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ํ๊ณ/๊ธฐํ ๊ธฐ์ค ๋ชจ๋ธ๊ณผ์ ๋น๊ต ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์ด์ ๊ธฐ๊ฐ ์ธ๊ด ํ์ ๋ก๊ทธ๋ก AAR์ ์ํํด ์คํ ์ฌ๋ก๋ฅผ ์์ธ๋ณ(์ค์ผ ๊ฐ๋ฆผยท์ ์กฐ๋ยท์์ ๊ฒฐํจยท๊ฒฝ๊ณ ๊ทผ์ฒยท์ ํ ํผ๋)๋ก ๋ถ๋ฅํ๊ณ , ๋ถํ ์๊ณ๊ฐยทํ์ต ๋ฐ์ดํฐยท๋ฑ๊ธ ๊ฒฝ๊ณ(๊ณผ์ ํ์ ์์ค ๋ฐ์)๋ฅผ ์ฌ๋ณด์ ํด ๋ณด๋ฅ ๊ตฌ๊ฐ์์ ๊ฐ์ ์ ๊ฒ์ฆํ ๋ค ๋น์ฉ ์ ๊ฐ์กยท์๊ฐ ๋จ์ถ๋ฅ ยทํฌ์ ํ์ ๊ธฐ๊ฐ 3๋ ํจ๊ณผ ์งํ๋ฅผ ์ฐ์๊ณผ ๋ฏผ๊ฐ๋๋ก ์ฐ์ถํ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.ensemble import RandomForestClassifier, GradientBoostingRegressor\n",
"from sklearn.model_selection import cross_val_predict\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ฅ๋น ์ธ๊ด ์ฌ์ง ์์ฑ๊ธฐ (์ค์ ์ฅ๋นยท๋ถ๋ยท์์น ์ฌ์ง์ด ์๋๋ผ ๋์๋ก ๊ทธ๋ฆฐ 64x64 ํฉ์ฑ ์ด๋ฏธ์ง)\n",
"# ๋์ฅ๋ ๊ธ์ ํ๋ฉด์ ๊ท ์ด(crack)ยท๋ถ์(corrosion)ยทํ์(damage)ยท๋์ (leak)๋ฅผ ๊ทธ๋ฆฌ๊ณ , ์กฐ๋ช
ํธ์ฐจ์ ์งํ(mud) ์ค์ผ์ ์๋๋ค.\n",
"# ์ด๋ฏธ์ง์ ํจ๊ป ์ฅ๋น ๊ด๋ฆฌ ๊ธฐ๋ก(์ ์ ์์ฝ)๊ณผ ์ ๋ต ๊ฒฐํจ ๋ง์คํฌยท์์๋ยท์ ๋น ๋ฑ๊ธ์ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"from scipy import ndimage as ndi\n",
"\n",
"SZ = 64\n",
"CLASSES = [\"none\", \"crack\", \"corrosion\", \"damage\", \"leak\"]\n",
"KO_CLS = {\"none\": \"๊ฒฐํจ ์์\", \"crack\": \"๊ท ์ด\", \"corrosion\": \"๋ถ์\", \"damage\": \"ํ์\", \"leak\": \"๋์ \"}\n",
"CLS_W = {\"none\": 0.0, \"crack\": 1.4, \"corrosion\": 1.0, \"damage\": 1.6, \"leak\": 0.6} # ์์๋ ๊ณ์ฐ์ฉ ๊ฒฐํจ ์ ํ ๊ฐ์ค์น\n",
"GRADE_NAME = {1: \"ํ์ฅ์ ๋น\", 2: \"์ผ์ ์ ๋น\", 3: \"์ฐฝ์ ๋น\"}\n",
"YY, XX = np.mgrid[0:SZ, 0:SZ]\n",
"\n",
"\n",
"def grade_of(idx):\n",
" # [TODO : ํ์ ์ค์ต 9] AAR ๊ธฐ๋ฐ ์ธ๊ด ์คํ ์ฌ๋ก ๋ถ์ ๋ฐ ์ปดํจํฐ ๋น์ ๋ชจ๋ธ ํ๋ผ๋ฏธํฐ ์ฌ๋ณด์ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def blob(rng, cy, cx, r, rough=0.35):\n",
" \"\"\"๋ถ๊ท์นํ ์ํ ์์ญ ๋ง์คํฌ.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 9] AAR ๊ธฐ๋ฐ ์ธ๊ด ์คํ ์ฌ๋ก ๋ถ์ ๋ฐ ์ปดํจํฐ ๋น์ ๋ชจ๋ธ ํ๋ผ๋ฏธํฐ ์ฌ๋ณด์ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def draw(rng, cls, sev):\n",
" \"\"\"๊ฒฐํจ ์ ํยท์ฌ๊ฐ๋ sev(0~1)์ ๋ง๋ ์ ๋ต ๋ง์คํฌ์ ์ ์ ๋ณด๋ฅผ ๋ง๋ ๋ค.\"\"\"\n",
" m = np.zeros((SZ, SZ), bool)\n",
" if cls == \"crack\":\n",
" y, x, ang = rng.uniform(12, 52), rng.uniform(12, 52), rng.uniform(0, np.pi)\n",
" for _ in range(int(20 + 40 * sev)):\n",
" ang += rng.normal(0, 0.35); y += np.sin(ang) * 1.2; x += np.cos(ang) * 1.2\n",
" if 0 <= int(y) < SZ and 0 <= int(x) < SZ:\n",
" m[int(y), int(x)] = True\n",
" m = ndi.binary_dilation(m, iterations=1 + int(sev > 0.6))\n",
" elif cls == \"corrosion\":\n",
" for _ in range(int(rng.integers(1, 4))):\n",
" m |= blob(rng, rng.uniform(14, 50), rng.uniform(14, 50), 3 + 9 * sev * rng.uniform(0.6, 1.1))\n",
" elif cls == \"damage\":\n",
" m = blob(rng, rng.uniform(20, 44), rng.uniform(20, 44), 6 + 12 * sev, 0.3)\n",
" elif cls == \"leak\":\n",
" x0, y0 = rng.uniform(16, 48), rng.uniform(8, 24)\n",
" w, ln = 2 + 4 * sev, 12 + 26 * sev\n",
" m = (np.abs(XX - x0 - 0.15 * (YY - y0)) < w * (1 - 0.4 * (YY - y0) / ln)) & (YY > y0) & (YY < y0 + ln)\n",
" return m\n",
"\n",
"\n",
"def make_images(n=600, seed=0, mud=0.0, p_none=0.25, dark=False):\n",
" \"\"\"๊ฐ์ ์ธ๊ด ์ฌ์ง n์ฅ(+์งํ ์ค์ผ mud ๋น์จ)๊ณผ ๊ด๋ฆฌ ๊ธฐ๋กยท์ ๋ต. ๋ฐํ: meta(DataFrame), imgs(n,64,64,3), masks(n,64,64).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" imgs, masks, rows = np.zeros((n, SZ, SZ, 3), np.float32), np.zeros((n, SZ, SZ), bool), []\n",
" for i in range(n):\n",
" cls = \"none\" if rng.random() < p_none else str(rng.choice(CLASSES[1:], p=[0.25, 0.3, 0.25, 0.2]))\n",
" sev = 0.0 if cls == \"none\" else float(rng.beta(1.6, 2.0))\n",
" base = np.array([0.42, 0.46, 0.40]) * (1 + 0.5 * ndi.gaussian_filter(rng.normal(size=(SZ, SZ)), 8)[..., None] * 3)\n",
" img = base + rng.normal(0, 0.012, (SZ, SZ, 3))\n",
" m = draw(rng, cls, sev)\n",
" a = ndi.gaussian_filter(m.astype(float), 0.8)[..., None] # ๊ฒฝ๊ณ๋ฅผ ๋ถ๋๋ฝ๊ฒ ์๋๋ค\n",
" if cls == \"crack\":\n",
" col = img * rng.uniform(0.2, 0.45)\n",
" elif cls == \"corrosion\":\n",
" col = np.array([0.55, 0.30, 0.12]) * rng.uniform(0.75, 1.15) * (0.85 + 0.3 * rng.random((SZ, SZ, 1)))\n",
" elif cls == \"damage\":\n",
" col = np.ones_like(img) * np.array([0.34, 0.34, 0.36]) * (1.0 - 0.45 * sev) * rng.uniform(0.75, 1.0)\n",
" rim = m & ~ndi.binary_erosion(m, iterations=2)\n",
" a = np.maximum(a, 0.0); img = np.where(rim[..., None], 0.7, img)\n",
" elif cls == \"leak\":\n",
" col = np.ones_like(img) * np.array([0.10, 0.09, 0.08]) * rng.uniform(0.8, 2.6)\n",
" else:\n",
" col = img\n",
" img = img * (1 - a) + col * a\n",
" if rng.random() < 0.35: # ๋ฌดํดํ ์ผ๋ฃฉ(๊ฒฐํจ ์๋): ์
์ ๊ฐ์ ๋ฒ์ง\n",
" st = blob(rng, rng.uniform(5, 58), rng.uniform(5, 58), rng.uniform(6, 14), 0.5)\n",
" al = ndi.gaussian_filter(st.astype(float), 2.5)[..., None] * rng.uniform(0.15, 0.4)\n",
" img = img * (1 - al) + np.array([0.50, 0.40, 0.30]) * al\n",
" if rng.random() < 0.3: # ๋ฌดํดํ ๊ธด ์คํฌ๋์น(๋ฐ์ ์ )\n",
" y0, x0, ang = rng.uniform(5, 58), rng.uniform(5, 58), rng.uniform(0, np.pi)\n",
" for t in np.linspace(0, 25, 40):\n",
" yy, xx = int(y0 + t * np.sin(ang)), int(x0 + t * np.cos(ang))\n",
" if 0 <= yy < SZ and 0 <= xx < SZ:\n",
" img[yy, xx] = np.clip(img[yy, xx] * 1.5, 0, 1)\n",
" if mud > 0: # ์งํ ์ค์ผ: ๊ฒฐํจ ์๋ฅผ ๋ฎ์ ์ ์๋ค(์ ๋ต ๋ง์คํฌ๋ ๊ทธ๋๋ก)\n",
" mm = np.zeros((SZ, SZ), bool)\n",
" while mm.mean() < mud:\n",
" mm |= blob(rng, rng.uniform(0, SZ), rng.uniform(0, SZ), rng.uniform(5, 11), 0.5)\n",
" img[mm] = np.array([0.38, 0.30, 0.22]) * (0.85 + 0.3 * rng.random((mm.sum(), 1)))\n",
" gain = rng.uniform(0.3, 0.55) if dark else rng.uniform(0.5, 1.15)\n",
" ang = rng.uniform(0, 2 * np.pi)\n",
" light = gain * (1 + 0.3 * (np.cos(ang) * (XX - 32) + np.sin(ang) * (YY - 32)) / 32)\n",
" imgs[i] = np.clip(img * light[..., None] + rng.normal(0, 0.025, (SZ, SZ, 3)), 0, 1)\n",
" masks[i] = m\n",
" area = float(m.mean())\n",
" depth = {\"none\": 0.0, \"crack\": 0.3 + 3.0 * sev, \"corrosion\": 0.1 + 1.5 * sev, \"damage\": 1.0 + 8.0 * sev, \"leak\": 0.0}[cls] * rng.lognormal(0, 0.1)\n",
" idx = float(np.clip(100 * (1 - np.exp(-(CLS_W[cls] * area * 9 + 0.05 * depth))) + rng.normal(0, 1.5), 0, 100))\n",
" rows.append({\"img_id\": f\"IMG{seed:02d}{i:04d}\", \"cls\": cls, \"sev\": sev, \"area_frac\": area, \"depth_mm\": depth, \"damage_idx\": idx,\n",
" \"grade\": int(grade_of(idx)), \"gain\": gain, \"mud\": mud})\n",
" meta = pd.DataFrame(rows)\n",
" # ์ฅ๋น ๊ด๋ฆฌ ๊ธฐ๋ก(์ ์ ์์ฝ): ์ฌ์ง ํ์ ๊ณผ ๋์กฐํ ๊ฐ\n",
" age = rng.integers(1, 25, n)\n",
" meta[\"day\"] = np.sort(rng.integers(0, 90, n))\n",
" meta[\"equip_type\"] = rng.choice([\"vehicle\", \"generator\", \"comm\", \"trailer\"], n, p=[0.4, 0.2, 0.2, 0.2])\n",
" meta[\"mfg_year\"] = 2026 - age\n",
" meta[\"op_hours\"] = np.clip(rng.normal(180 * age, 400, n) + 8 * meta[\"damage_idx\"], 10, 20000)\n",
" meta[\"env\"] = rng.choice([\"coastal\", \"inland\"], n, p=[0.3, 0.7])\n",
" reg = np.clip(meta[\"grade\"] + rng.choice([-1, 0, 0, 0, 0, 0, 1, 2], n), 1, 3) # ์ฅ๋น ๊ด๋ฆฌ ๊ธฐ๋ก์ ์ง์ ์ ๋น ๋ฑ๊ธ\n",
" meta[\"reg_grade\"] = reg\n",
" meta[\"emergency\"] = rng.random(n) < 0.04 # ๊ธด๊ธ ์ ๊ฒ ์์ฒญ(ํ์ง ๋ถ๋์ด์ด๋ ๋ณด์กด)\n",
" return meta, imgs, masks\n",
"# ---- ์ ์ฒ๋ฆฌ(์กฐ๋ช
๋ณด์ ยทํ์คํ)์ ๊ฒฐํจ ํ๋ณด ๋ถํ , ํน์ง ์ถ์ถ (cv2 ๋์ numpyยทscipy ์ฌ์ฉ) ----\n",
"def correct(img):\n",
" \"\"\"์กฐ๋ช
๋ณด์ : ๋ฐ๊ธฐ์ ํ๋ฉด(์กฐ๋ช
๊ธฐ์ธ๊ธฐ)์ ์ ํฉํด ๋๋๋ค. ์ด๋์ด ๊ฒฐํจ ํ์๋ ์ ์ธํ๊ณ ๋ ๋ฒ ๋ฐ๋ณต ์ ํฉํ๋ค. ๋ฐํ: ๋ณด์ ๋ฐ๊ธฐ(์ ์ ๋ฉด ์ฝ 1), ์ ๋น์จ.\"\"\"\n",
" g = img.mean(axis=2)\n",
" A = np.c_[np.ones(SZ * SZ), XX.ravel() / SZ, YY.ravel() / SZ]\n",
" keep = np.ones(SZ * SZ, bool)\n",
" for _ in range(3):\n",
" coef, *_ = np.linalg.lstsq(A[keep], g.ravel()[keep], rcond=None)\n",
" plane = (A @ coef).reshape(SZ, SZ)\n",
" keep = (np.abs(g - plane) < 0.08 * np.maximum(plane, 1e-3)).ravel()\n",
" if keep.sum() < 200:\n",
" keep[:] = True\n",
" c = g / np.maximum(plane, 1e-3)\n",
" chroma = img[..., 0] / (img.sum(axis=2) + 1e-6)\n",
" return c, chroma\n",
"\n",
"\n",
"SEGP = {\"dark\": 0.7, \"rust\": 0.5, \"rim\": 1.45} # ๋ถํ ์๊ณ๊ฐ(์ด๋์ ๋ณด์ ๋ฐ๊ธฐ, ๋ถ์ ๋น์จ, ๋ฐ์ ํ
๋๋ฆฌ): ์ฌ๋ณด์ ๋์\n",
"\n",
"\n",
"def segment(img):\n",
" \"\"\"๊ฒฐํจ ํ๋ณด ๋ง์คํฌ: ์ด๋์ด ์์ญ(๊ท ์ดยท๋์ ยทํ์) + ๋ถ์ ๊ฐ์ ์์ญ(๋ถ์) + ๋ฐ์ ํ
๋๋ฆฌ(ํ์).\"\"\"\n",
" c, chroma = correct(img)\n",
" dark = c < SEGP[\"dark\"]\n",
" rust = (chroma > SEGP[\"rust\"]) & (c > 0.45)\n",
" rim = c > SEGP[\"rim\"]\n",
" m = dark | rust | ndi.binary_dilation(rim, iterations=1) & ndi.binary_dilation(dark, iterations=3)\n",
" m = ndi.binary_opening(m, structure=np.ones((2, 2))) if m.sum() > 12 else m\n",
" return m, c, chroma, dark, rust, rim\n",
"\n",
"\n",
"def extract(img):\n",
" \"\"\"๋ถํ ๊ฒฐ๊ณผ์ ์ยท๋ชจ์์์ 24๊ฐ ํน์ง์ ๋ง๋ ๋ค.\"\"\"\n",
" m, c, chroma, dark, rust, rim = segment(img)\n",
" lab, n = ndi.label(m)\n",
" area = float(m.sum())\n",
" if n:\n",
" sizes = ndi.sum(m, lab, range(1, n + 1))\n",
" k = int(np.argmax(sizes)) + 1\n",
" sl = ndi.find_objects(lab)[k - 1]\n",
" h, w = sl[0].stop - sl[0].start, sl[1].stop - sl[1].start\n",
" big = float(sizes.max())\n",
" elong, fill = max(h, w) / max(min(h, w), 1), big / (h * w)\n",
" else:\n",
" big, elong, fill = 0.0, 1.0, 0.0\n",
" gy, gx = np.gradient(c)\n",
" mag = np.hypot(gx, gy)\n",
" inside = m.sum() > 0\n",
" return {\"area_px\": area, \"area_frac\": area / SZ ** 2, \"n_comp\": float(n), \"big_px\": big, \"elong\": elong, \"fill\": fill,\n",
" \"dark_frac\": float(dark.mean()), \"rust_frac\": float(rust.mean()), \"rim_frac\": float(rim.mean()),\n",
" \"c_mean\": float(c.mean()), \"c_std\": float(c.std()), \"c_min\": float(c.min()), \"edge_mean\": float(mag.mean()), \"edge_p95\": float(np.quantile(mag, 0.95)),\n",
" \"chroma_mean\": float(chroma.mean()), \"chroma_max\": float(chroma.max()),\n",
" \"in_dark\": float(c[m].mean()) if inside else 1.0, \"in_chroma\": float(chroma[m].mean()) if inside else float(chroma.mean()),\n",
" \"ring_contrast\": float(c[ndi.binary_dilation(m, iterations=3) & ~m].mean() - c[m].mean()) if inside and (~m).any() else 0.0,\n",
" \"thin\": float(1 - fill), \"gain\": float(img.mean()), \"sat\": float((img.max(axis=2) - img.min(axis=2)).mean()),\n",
" \"cy\": float(ndi.center_of_mass(m)[0] / SZ) if inside else 0.5, \"cx\": float(ndi.center_of_mass(m)[1] / SZ) if inside else 0.5}\n",
"\n",
"\n",
"def feature_table(imgs):\n",
" return pd.DataFrame([extract(im) for im in imgs])\n",
"# ---- ํ์ ํ์ดํ๋ผ์ธ: ๊ฒฐํจ ์ ํ ๋ถ๋ฅ + ์์๋ ํ๊ท(๋ถ๋ฅ ํ๋ฅ ํฌํจ) -> ์ ๋น ๋ฑ๊ธ ----\n",
"def fit_pipeline(meta, F):\n",
" \"\"\"๊ฒฐํจ ์ ํ ๋ถ๋ฅ๊ธฐ์ ์์๋ ํ๊ท๊ธฐ๋ฅผ ํ์ตํ๋ค. ํ๊ท๊ธฐ ์
๋ ฅ์ ๋ถ๋ฅ ํ๋ฅ ์ ๊ต์ฐจ ์์ธก์ผ๋ก ๋ง๋ค์ด ๋์ค์ ๋ง๋๋ค.\"\"\"\n",
" clf = RandomForestClassifier(n_estimators=150, random_state=0, n_jobs=1)\n",
" P = cross_val_predict(clf, F, meta[\"cls\"], cv=4, method=\"predict_proba\")\n",
" clf.fit(F, meta[\"cls\"])\n",
" reg = GradientBoostingRegressor(n_estimators=200, max_depth=3, learning_rate=0.06, random_state=0).fit(np.c_[F.values, P], meta[\"damage_idx\"])\n",
" return {\"clf\": clf, \"reg\": reg, \"cols\": list(F.columns)}\n",
"\n",
"\n",
"def predict_pipeline(pl, F):\n",
" X = F[pl[\"cols\"]]\n",
" P = pl[\"clf\"].predict_proba(X)\n",
" idx = np.clip(pl[\"reg\"].predict(np.c_[X.values, P]), 0, 100)\n",
" return pl[\"clf\"].classes_[P.argmax(axis=1)], P, idx, P.max(axis=1)\n",
"# ---- ์ฌ์ง ํ์ง ๊ฒ์ดํธ: ์งํยท์ค์ผ ๊ฐ๋ฆผ ์ถ์ ๊ณผ ํ์ ์ ๋ขฐ๋ ์ง์ ----\n",
"def mud_est(img):\n",
" \"\"\"์งํ(๊ฐ์, ์ฑ๋ ๋ฎ์ ๋ถ์ ๊ธฐ) ํ์ ๋น์จ์ ์ถ์ ํ๋ค. 3x3 ํ๊ท ์ผ๋ก ์ก์์ ์ค์ธ ๋ค ์ ๋น์จ๋ก ํ๋จํ๋ฉฐ, ์ค์ ๊ฐ๋ฆผ ๋น์จ๋ณด๋ค ์๊ฒ ๋์ค์ง๋ง ์์๋ ์ ์ง๋๋ค.\"\"\"\n",
" sm = ndi.uniform_filter(img, size=(3, 3, 1))\n",
" s = sm.sum(axis=2) + 1e-6\n",
" rr, gg = sm[..., 0] / s, sm[..., 1] / s\n",
" return float(((rr > 0.385) & (rr < 0.46) & (gg < 0.325)).mean())\n",
"\n",
"\n",
"def confidence(P, idx, F, mud):\n",
" \"\"\"ํ์ ์ ๋ขฐ๋ ์ง์ 0~100 = 50 x ๋ถ๋ฅ ํ๋ฅ ์ต๋๊ฐ + 35 x ๋ฑ๊ธ ๊ฒฝ๊ณ๊น์ง ๊ฑฐ๋ฆฌ(8์ ์์ ํฌํ) + 15 x ์ฌ์ง ํ์ง. ์ค์ผ ์์ฌ(mud ์ถ์ 1% ์ด์)์ด๋ฉด ์ํ 75๋ก ์ ํํ๋ค.\"\"\"\n",
" bd = np.minimum(np.abs(idx - 35), np.abs(idx - 65))\n",
" qual = np.clip(1.0 - np.clip(0.55 - F[\"gain\"].values, 0, None) * 2 - np.clip(0.9 - F[\"c_mean\"].values, 0, None), 0, 1)\n",
" c = 50 * P.max(axis=1) + 35 * np.clip(bd / 8, 0, 1) + 15 * qual\n",
" return np.where(np.asarray(mud) >= 0.01, np.minimum(c, 75.0), c)\n",
"\n",
"mtr, itr, _ = make_images(600, seed=1)\n",
"Ftr = feature_table(itr)\n",
"pl0 = fit_pipeline(mtr, Ftr)\n",
"LOSS_UNDER, LOSS_OVER = 20.0, 6.0 # ๊ณผ์/๊ณผ๋ ํ์ 1๋ฑ๊ธ๋น ์์ค(๋ง์, ๊ฐ์ )\n",
"\n",
"\n",
"def ops(seed, n):\n",
" \"\"\"์ด์ ์ฌ์ง ๋ฌถ์: ์ ์กฐ๋ 1/3 + ์งํ 5% 1/3 + ์ ์ 1/3 (ํ์ต ๋์ ๋ถํฌ๊ฐ ๋ค๋ฅด๋ค).\"\"\"\n",
" parts = [make_images(n // 3, seed=seed, dark=True), make_images(n // 3, seed=seed + 1, mud=0.05), make_images(n // 3, seed=seed + 2)]\n",
" return pd.concat([p[0] for p in parts], ignore_index=True), np.concatenate([p[1] for p in parts]), np.concatenate([p[2] for p in parts])\n",
"\n",
"\n",
"def loss(true, g):\n",
" d = np.asarray(g) - np.asarray(true)\n",
" return np.where(d < 0, -d * LOSS_UNDER, d * LOSS_OVER)\n",
"\n",
"\n",
"def grade_shift(idx, delta):\n",
" \"\"\"๊ณผ์ ํ์ ์ ์ค์ด๊ธฐ ์ํด ๋ฑ๊ธ ๊ฒฝ๊ณ๋ฅผ delta๋งํผ ๋ฎ์ถ๋ค(35 -> 35-delta, 65 -> 65-delta).\"\"\"\n",
" return np.where(np.asarray(idx) >= 65 - delta, 3, np.where(np.asarray(idx) >= 35 - delta, 2, 1))\n",
"\n",
"\n",
"def evaluate(pl, F, m, ims, delta=0):\n",
" cls, P, idx, _ = predict_pipeline(pl, F)\n",
" mud = np.array([mud_est(x) for x in ims])\n",
" conf = confidence(P, idx, F, mud)\n",
" g = grade_shift(idx, delta)\n",
" auto = (conf >= 80) & (mud < 0.03) & (F[\"gain\"].values >= 0.17)\n",
" ok = g == m[\"grade\"].values\n",
" return {\"cls\": cls, \"idx\": idx, \"g\": g, \"mud\": mud, \"conf\": conf, \"ok\": ok, \"mae\": float(np.mean(np.abs(idx - m[\"damage_idx\"].values))), \"under\": int((g < m[\"grade\"].values).sum()),\n",
" \"loss\": float(loss(m[\"grade\"].values, g).mean()), \"auto_n\": int(auto.sum()), \"auto_acc\": float(ok[auto].mean()) if auto.any() else np.nan}\n",
"\n",
"\n",
"# ---------------- 1) AAR: ์ด์ ๋ก๊ทธ ๋ถ์(์ด๋ ๊ตฌ๊ฐ A, 360์ฅ) ----------------\n",
"mA, iA, kA = ops(60, 360)\n",
"rng = np.random.default_rng(0)\n",
"perm = rng.permutation(len(mA))\n",
"FA = feature_table(iA)\n",
"ev = evaluate(pl0, FA, mA, iA)\n",
"print(f\"[1] AAR ๋์ ์ด์ ์ฌ์ง {len(mA)}์ฅ(์ ์กฐ๋ยท์งํ 5%ยท์ ์ ํผํฉ): ๋ฑ๊ธ ์ผ์น์จ {ev['ok'].mean():.1%}, ์์๋ MAE {ev['mae']:.2f}, ๊ณผ์ ํ์ {ev['under']}์ฅ, ์ฅ๋น ์คํ ์์ค {ev['loss']:.2f}๋ง์\")\n",
"wrong = np.where(~ev[\"ok\"])[0]\n",
"\n",
"\n",
"def cause(i):\n",
" if ev[\"mud\"][i] >= 0.01: return \"์ค์ผ ๊ฐ๋ฆผ\"\n",
" if FA[\"gain\"].iloc[i] < 0.2: return \"์ ์กฐ๋\"\n",
" if mA[\"cls\"][i] != \"none\" and mA[\"area_frac\"][i] < 0.02: return \"์์ ๊ฒฐํจ ๋๋ฝยท๊ณผ์\"\n",
" if ev[\"cls\"][i] != mA[\"cls\"][i]: return \"๊ฒฐํจ ์ ํ ํผ๋\"\n",
" if min(abs(ev[\"idx\"][i] - 35), abs(ev[\"idx\"][i] - 65)) < 6: return \"๋ฑ๊ธ ๊ฒฝ๊ณ ๊ทผ์ฒ\"\n",
" return \"๊ธฐํ\"\n",
"\n",
"\n",
"cs = pd.Series([cause(i) for i in wrong]).value_counts()\n",
"print(f\" ์คํ {len(wrong)}์ฅ์ ์์ธ ๋ถ๋ฅ:\", \", \".join(f\"{k} {v}์ฅ({v / len(wrong):.0%})\" for k, v in cs.items()))\n",
"print(f\" ์คํ ์ค ๊ณผ์ ํ์ (์ค์ ๋ณด๋ค ๋ฎ์ ๋ฑ๊ธ) {ev['under']}์ฅ / ๊ณผ๋ {int((ev['g'] > mA['grade'].values).sum())}์ฅ -> ๊ณผ์ ํ์ ์ด ์์ค ๋จ๊ฐ๊ฐ {LOSS_UNDER / LOSS_OVER:.1f}๋ฐฐ ๋์ ์ฐ์ ๊ต์ ๋์\")\n",
"\n",
"# ---------------- 2) ์ฌ๋ณด์ : ๋ถํ ์๊ณ๊ฐ -> ์ด์ ๋ฐ์ดํฐ ์ฌํ์ต -> ๋ฑ๊ธ ๊ฒฝ๊ณ ์ด๋ ----------------\n",
"A1, A2 = perm[:240], perm[240:]\n",
"cache = [correct(iA[i]) for i in A1]\n",
"\n",
"\n",
"def mask_from(c, chroma, dt, rt, rim=1.45):\n",
" dark, rust, r = c < dt, (chroma > rt) & (c > 0.45), c > rim\n",
" m = dark | rust | ndi.binary_dilation(r, iterations=1) & ndi.binary_dilation(dark, iterations=3)\n",
" return ndi.binary_opening(m, structure=np.ones((2, 2))) if m.sum() > 12 else m\n",
"\n",
"\n",
"grid = []\n",
"for dt in (0.6, 0.7, 0.75, 0.8):\n",
" for rt in (0.44, 0.5, 0.54):\n",
" iou = np.mean([(mask_from(c, ch, dt, rt) & kA[i]).sum() / max((mask_from(c, ch, dt, rt) | kA[i]).sum(), 1) for (c, ch), i in zip(cache, A1)])\n",
" grid.append((dt, rt, iou))\n",
"gd = pd.DataFrame(grid, columns=[\"dark ์๊ณ๊ฐ\", \"rust ์๊ณ๊ฐ\", \"ํ๊ท IoU\"])\n",
"bst = gd.loc[gd[\"ํ๊ท IoU\"].idxmax()]\n",
"cur = gd[(gd[\"dark ์๊ณ๊ฐ\"] == 0.7) & (gd[\"rust ์๊ณ๊ฐ\"] == 0.5)][\"ํ๊ท IoU\"].iloc[0]\n",
"print(f\"\\n[2] ๋ถํ ์๊ณ๊ฐ ์ฌ๋ณด์ (์ด์ ์ฌ์ง 240์ฅ IoU): ํํ(0.70, 0.50) {cur:.3f} -> ์ต์ ({bst['dark ์๊ณ๊ฐ']:.2f}, {bst['rust ์๊ณ๊ฐ']:.2f}) {bst['ํ๊ท IoU']:.3f}\")\n",
"SEGP[\"dark\"], SEGP[\"rust\"] = float(bst[\"dark ์๊ณ๊ฐ\"]), float(bst[\"rust ์๊ณ๊ฐ\"])\n",
"FA2 = feature_table(iA)\n",
"mm = pd.concat([mtr, mA.iloc[A1]], ignore_index=True)\n",
"pl1 = fit_pipeline(mm, pd.concat([Ftr, FA2.iloc[A1]], ignore_index=True)) # ์ด์ ๋ถํฌ ์ฌ์ง 240์ฅ์ ์ถ๊ฐ ํ์ต\n",
"ev2 = evaluate(pl1, FA2.iloc[A2].reset_index(drop=True), mA.iloc[A2].reset_index(drop=True), iA[A2])\n",
"costs = []\n",
"for dlt in (0, 2, 4, 6, 8):\n",
" r = evaluate(pl1, FA2.iloc[A2].reset_index(drop=True), mA.iloc[A2].reset_index(drop=True), iA[A2], delta=dlt)\n",
" costs.append((dlt, r[\"loss\"], r[\"under\"], r[\"ok\"].mean()))\n",
"cd = pd.DataFrame(costs, columns=[\"๊ฒฝ๊ณ ์ด๋ delta\", \"์ฅ๋น ์์ค(๋ง์)\", \"๊ณผ์ ํ์ \", \"์ผ์น์จ\"])\n",
"best_d = int(cd.loc[cd[\"์ฅ๋น ์์ค(๋ง์)\"].idxmin(), \"๊ฒฝ๊ณ ์ด๋ delta\"])\n",
"print(\" ๋ฑ๊ธ ๊ฒฝ๊ณ ์ด๋(๋ณด์ ์ฉ 120์ฅ, ๊ณผ์ ํ์ ์์ค ๋ฐ์)\\n\" + cd.round(3).to_string(index=False))\n",
"if best_d == 0:\n",
" print(\" ๊ฒฝ๊ณ๋ฅผ ๋ฎ์ถ๋ฉด ๊ณผ๋ ํ์ ์ด ๋์ด ์์ค์ด ์คํ๋ ค ์ปค์ง๋ฏ๋ก ๊ฒฝ๊ณ ์ด๋์ ์ฑํํ์ง ์๋๋ค(delta=0 ์ ์ง). ๊ณผ์ ํ์ ์ ๊ฒฝ๊ณ๊ฐ ์๋๋ผ ์ค์ผ ๊ฐ๋ฆผ์์ ์ฃผ๋ก ๋ฐ์ํ๋ค.\")\n",
"else:\n",
" print(f\" ์์ค์ด ๊ฐ์ฅ ์์ ๊ฒฝ๊ณ ์ด๋ delta={best_d}์ ์ฑํ(๊ณผ์ ํ์ ์ ์ค์ด๋ ๋ฐฉํฅ์ด๋ฉฐ ์ฌ๋ ์น์ธ ํ ์ ์ฉ)\")\n",
"\n",
"# ---------------- 3) ๋ณด๋ฅ ๊ตฌ๊ฐ B(360์ฅ, ์ฌ๋ณด์ ์ ์ฐ์ง ์์) ๊ฒ์ฆ ----------------\n",
"mB, iB, kB = ops(70, 360)\n",
"SEGP[\"dark\"], SEGP[\"rust\"] = 0.7, 0.5\n",
"FB0 = feature_table(iB)\n",
"e0 = evaluate(pl0, FB0, mB, iB)\n",
"SEGP[\"dark\"], SEGP[\"rust\"] = float(bst[\"dark ์๊ณ๊ฐ\"]), float(bst[\"rust ์๊ณ๊ฐ\"])\n",
"FB1 = feature_table(iB)\n",
"e1 = evaluate(pl1, FB1, mB, iB, delta=best_d)\n",
"tab = pd.DataFrame({\"์ฌ๋ณด์ ์ \": [e0[\"ok\"].mean(), e0[\"mae\"], e0[\"under\"], e0[\"loss\"], e0[\"auto_n\"], e0[\"auto_acc\"]], \"์ฌ๋ณด์ ํ\": [e1[\"ok\"].mean(), e1[\"mae\"], e1[\"under\"], e1[\"loss\"], e1[\"auto_n\"], e1[\"auto_acc\"]]},\n",
" index=[\"๋ฑ๊ธ ์ผ์น์จ\", \"์์๋ MAE\", \"๊ณผ์ ํ์ (์ฅ)\", \"์ฅ๋น ์คํ ์์ค(๋ง์)\", \"์๋ ์ฒ๋ฆฌ ๊ฑด์\", \"์๋ ์ฒ๋ฆฌ ์ผ์น์จ\"])\n",
"print(\"\\n[3] ๋ณด๋ฅ ๊ตฌ๊ฐ B ๊ฒ์ฆ\\n\" + tab.round(3).to_string())\n",
"print(f\" ์คํ ์์ค {1 - e1['loss'] / e0['loss']:.1%} ๊ฐ์, ๊ณผ์ ํ์ {e0['under']} -> {e1['under']}์ฅ. ์ค์ผ ๊ฐ๋ฆผ ์ฌ์ง์ ์ ๋ณด ์์ฒด๊ฐ ๊ฐ๋ ค์ ธ ์ฌ๋ณด์ ์ผ๋ก ํด๊ฒฐ๋์ง ์์ผ๋ฏ๋ก ์ฌ์ดฌ์์ด ํ์ํ๋ค.\")\n",
"\n",
"# ---------------- 4) ROI 3๋ ์งํ(๊ต์ก์ฉ ๊ฐ์ ) ----------------\n",
"N_YR, MIN_FULL, MIN_QUICK = 20000, 15.0, 4.0 # ์ฐ ํ์ ์ฌ์ง ์, ์ ๋ฐ ํ๋
/๊ฐ์ด ์น์ธ ์์(๋ถ)\n",
"WAGE, INV, OPEX = 1.2, 2500.0, 150.0 # ๊ฒ์ฌ๊ด ์ธ๊ฑด๋น(๋ง์/์), ๋์
๋น์ฉ, ์ฐ ์ด์๋น(๋ง์)\n",
"full_b, full_a = 1 - e0[\"auto_n\"] / len(mB), 1 - e1[\"auto_n\"] / len(mB)\n",
"\n",
"\n",
"def roi(loss_b, loss_a, fb, fa, n=N_YR, inv=INV):\n",
" t_b = fb * MIN_FULL + (1 - fb) * MIN_QUICK\n",
" t_a = fa * MIN_FULL + (1 - fa) * MIN_QUICK\n",
" save_loss = (loss_b - loss_a) * n\n",
" save_time = (MIN_FULL - t_a) / 60 * WAGE * n # ๊ดํ(์ ์ ์ ๋ฐ ํ๋
) ๋๋น ํ๋
์ธ๊ฑด๋น ์ ๊ฐ\n",
" save = save_loss + save_time - OPEX\n",
" return save, 1 - t_a / MIN_FULL, inv / max(save / 12, 1e-9), save_loss, save_time\n",
"\n",
"\n",
"sv, tr, pb, sl, st = roi(e0[\"loss\"], e1[\"loss\"], full_b, full_a)\n",
"print(f\"\\n[4] ROI 3๋ ์งํ [๊ต์ก์ฉ ๊ฐ์ : ์ฐ {N_YR}์ฅ, ์ ๋ฐ ํ๋
{MIN_FULL:.0f}๋ถ, ๊ฐ์ด ์น์ธ {MIN_QUICK:.0f}๋ถ, ์ธ๊ฑด๋น {WAGE}๋ง์/์, ๋์
{INV:.0f}๋ง์, ์ด์ {OPEX:.0f}๋ง์/๋
]\")\n",
"print(f\" (1) ์ฐ๊ฐ ๋น์ฉ ์ ๊ฐ์ก {sv:,.0f}๋ง์(์คํ ์์ค ๊ฐ์ {sl:,.0f} + ํ๋
์ธ๊ฑด๋น {st:,.0f} - ์ด์๋น) (2) ํ๋
์๊ฐ ๋จ์ถ๋ฅ {tr:.1%}(๊ดํ ์ ์ ์ ๋ฐ ํ๋
๋๋น) (3) ํฌ์ ํ์ ๊ธฐ๊ฐ {pb:.1f}๊ฐ์\")\n",
"print(\" ์ฐ์: ์ ๊ฐ์ก = (์ฌ๋ณด์ ์ - ํ ์ฅ๋น ์คํ ์์ค) x ์ฐ ์ฅ์ + (์ ๋ฐ ํ๋
- ํ๊ท ํ๋
์๊ฐ)/60 x ์ธ๊ฑด๋น x ์ฐ ์ฅ์ - ์ด์๋น\")\n",
"print(\" ์ฅ์ ๋ฏผ๊ฐ๋:\", \"; \".join(f\"{n}์ฅ -> ์ ๊ฐ {roi(e0['loss'], e1['loss'], full_b, full_a, n=n)[0]:,.0f}๋ง์, ํ์ {roi(e0['loss'], e1['loss'], full_b, full_a, n=n)[2]:.0f}๊ฐ์\" for n in (5000, 10000, 20000, 40000)))\n",
"print(\" ๋น๊ด(์ฌ๋ณด์ ํจ๊ณผ ์์, ํ๋
์๊ฐ ์ ๊ฐ๋ง):\", f\"์ ๊ฐ {roi(e0['loss'], e0['loss'], full_b, full_a)[0]:,.0f}๋ง์, ํ์ {roi(e0['loss'], e0['loss'], full_b, full_a)[2]:.0f}๊ฐ์\")\n",
"print(\" ๊ฐ์ ๊ฐ์ด๋ฏ๋ก ์ฌ์ ยท๊ธฐํ ๋ถ์์ ์ค์ ๋จ๊ฐ๋ก ๋ค์ ์ฐ์ ํด์ผ ํ๋ค. ์ฌ๋ณด์ ๋ ์๊ณ๊ฐยท๊ฒฝ๊ณ๋ ์ ๋น๊ฐ๋
๊ด ์น์ธ ํ ์ ์ฉํ๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(1, 3, figsize=(15, 4.2))\n",
"ax[0].bar(range(len(cs)), cs.values); ax[0].set_xticks(range(len(cs))); ax[0].set_xticklabels([f\"C{i + 1}\" for i in range(len(cs))]); ax[0].set_title(\"AAR: misjudgment causes (count)\")\n",
"ax[1].plot(cd[\"๊ฒฝ๊ณ ์ด๋ delta\"], cd[\"์ฅ๋น ์์ค(๋ง์)\"], \"o-\"); ax[1].set_title(\"Loss per photo vs grade-boundary shift\"); ax[1].set_xlabel(\"delta\")\n",
"x = np.arange(3); ax[2].bar(x - .2, [e0[\"ok\"].mean() * 100, e0[\"under\"], e0[\"loss\"]], .4, label=\"before\"); ax[2].bar(x + .2, [e1[\"ok\"].mean() * 100, e1[\"under\"], e1[\"loss\"]], .4, label=\"after\")\n",
"ax[2].set_xticks(x); ax[2].set_xticklabels([\"accuracy %\", \"under-grade n\", \"loss/photo\"]); ax[2].legend(); ax[2].set_title(\"Hold-out B: before vs after\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
09 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[1] AAR ๋์ ์ด์ ์ฌ์ง 360์ฅ(์ ์กฐ๋ยท์งํ 5%ยท์ ์ ํผํฉ): ๋ฑ๊ธ ์ผ์น์จ 83.1%, ์์๋ MAE 8.72, ๊ณผ์ ํ์ 22์ฅ, ์ฅ๋น ์คํ ์์ค 1.99๋ง์\n",
" ์คํ 61์ฅ์ ์์ธ ๋ถ๋ฅ: ์ค์ผ ๊ฐ๋ฆผ 29์ฅ(48%), ๊ฒฐํจ ์ ํ ํผ๋ 9์ฅ(15%), ์ ์กฐ๋ 8์ฅ(13%), ๊ธฐํ 8์ฅ(13%), ๋ฑ๊ธ ๊ฒฝ๊ณ ๊ทผ์ฒ 7์ฅ(11%)\n",
" ์คํ ์ค ๊ณผ์ ํ์ (์ค์ ๋ณด๋ค ๋ฎ์ ๋ฑ๊ธ) 22์ฅ / ๊ณผ๋ 39์ฅ -> ๊ณผ์ ํ์ ์ด ์์ค ๋จ๊ฐ๊ฐ 3.3๋ฐฐ ๋์ ์ฐ์ ๊ต์ ๋์\n",
"\n",
"[2] ๋ถํ ์๊ณ๊ฐ ์ฌ๋ณด์ (์ด์ ์ฌ์ง 240์ฅ IoU): ํํ(0.70, 0.50) 0.495 -> ์ต์ (0.75, 0.44) 0.528\n",
" ๋ฑ๊ธ ๊ฒฝ๊ณ ์ด๋(๋ณด์ ์ฉ 120์ฅ, ๊ณผ์ ํ์ ์์ค ๋ฐ์)\n",
" ๊ฒฝ๊ณ ์ด๋ delta ์ฅ๋น ์์ค(๋ง์) ๊ณผ์ ํ์ ์ผ์น์จ\n",
" 0 1.483 5 0.850\n",
" 2 1.733 5 0.808\n",
" 4 1.933 5 0.775\n",
" 6 2.133 5 0.742\n",
" 8 1.950 3 0.733\n",
" ๊ฒฝ๊ณ๋ฅผ ๋ฎ์ถ๋ฉด ๊ณผ๋ ํ์ ์ด ๋์ด ์์ค์ด ์คํ๋ ค ์ปค์ง๋ฏ๋ก ๊ฒฝ๊ณ ์ด๋์ ์ฑํํ์ง ์๋๋ค(delta=0 ์ ์ง). ๊ณผ์ ํ์ ์ ๊ฒฝ๊ณ๊ฐ ์๋๋ผ ์ค์ผ ๊ฐ๋ฆผ์์ ์ฃผ๋ก ๋ฐ์ํ๋ค.\n",
"\n",
"[3] ๋ณด๋ฅ ๊ตฌ๊ฐ B ๊ฒ์ฆ\n",
" ์ฌ๋ณด์ ์ ์ฌ๋ณด์ ํ\n",
"๋ฑ๊ธ ์ผ์น์จ 0.819 0.853\n",
"์์๋ MAE 7.579 6.559\n",
"๊ณผ์ ํ์ (์ฅ) 26.000 25.000\n",
"์ฅ๋น ์คํ ์์ค(๋ง์) 2.194 2.022\n",
"์๋ ์ฒ๋ฆฌ ๊ฑด์ 150.000 151.000\n",
"์๋ ์ฒ๋ฆฌ ์ผ์น์จ 0.980 0.980\n",
" ์คํ ์์ค 7.8% ๊ฐ์, ๊ณผ์ ํ์ 26 -> 25์ฅ. ์ค์ผ ๊ฐ๋ฆผ ์ฌ์ง์ ์ ๋ณด ์์ฒด๊ฐ ๊ฐ๋ ค์ ธ ์ฌ๋ณด์ ์ผ๋ก ํด๊ฒฐ๋์ง ์์ผ๋ฏ๋ก ์ฌ์ดฌ์์ด ํ์ํ๋ค.\n",
"\n",
"[4] ROI 3๋ ์งํ [๊ต์ก์ฉ ๊ฐ์ : ์ฐ 20000์ฅ, ์ ๋ฐ ํ๋
15๋ถ, ๊ฐ์ด ์น์ธ 4๋ถ, ์ธ๊ฑด๋น 1.2๋ง์/์, ๋์
2500๋ง์, ์ด์ 150๋ง์/๋
]\n",
" (1) ์ฐ๊ฐ ๋น์ฉ ์ ๊ฐ์ก 5,140๋ง์(์คํ ์์ค ๊ฐ์ 3,444 + ํ๋
์ธ๊ฑด๋น 1,846 - ์ด์๋น) (2) ํ๋
์๊ฐ ๋จ์ถ๋ฅ 30.8%(๊ดํ ์ ์ ์ ๋ฐ ํ๋
๋๋น) (3) ํฌ์ ํ์ ๊ธฐ๊ฐ 5.8๊ฐ์\n",
" ์ฐ์: ์ ๊ฐ์ก = (์ฌ๋ณด์ ์ - ํ ์ฅ๋น ์คํ ์์ค) x ์ฐ ์ฅ์ + (์ ๋ฐ ํ๋
- ํ๊ท ํ๋
์๊ฐ)/60 x ์ธ๊ฑด๋น x ์ฐ ์ฅ์ - ์ด์๋น\n",
" ์ฅ์ ๋ฏผ๊ฐ๋: 5000์ฅ -> ์ ๊ฐ 1,173๋ง์, ํ์ 26๊ฐ์; 10000์ฅ -> ์ ๊ฐ 2,495๋ง์, ํ์ 12๊ฐ์; 20000์ฅ -> ์ ๊ฐ 5,140๋ง์, ํ์ 6๊ฐ์; 40000์ฅ -> ์ ๊ฐ 10,430๋ง์, ํ์ 3๊ฐ์\n",
" ๋น๊ด(์ฌ๋ณด์ ํจ๊ณผ ์์, ํ๋
์๊ฐ ์ ๊ฐ๋ง): ์ ๊ฐ 1,696๋ง์, ํ์ 18๊ฐ์\n",
" ๊ฐ์ ๊ฐ์ด๋ฏ๋ก ์ฌ์ ยท๊ธฐํ ๋ถ์์ ์ค์ ๋จ๊ฐ๋ก ๋ค์ ์ฐ์ ํด์ผ ํ๋ค. ์ฌ๋ณด์ ๋ ์๊ณ๊ฐยท๊ฒฝ๊ณ๋ ์ ๋น๊ฐ๋
๊ด ์น์ธ ํ ์ ์ฉํ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
10. ์ฅ๋น ๊ฒฐํจ ์ฌ์ง ๊ธฐ๋ฐ AI ์ธ๊ด ๊ฒ์ฌ ๋ฐ ์์๋ ์๋ ํ์ ๋๊ตฌ ํตํฉ ์์ฑ\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ฅ๋น ๊ฒฐํจ ์ฌ์ง ๊ธฐ๋ฐ AI ์ธ๊ด ๊ฒ์ฌ ๋ฐ ์์๋ ์๋ ํ์ ๋๊ตฌ๋ฅผ ์์ฑํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ฅ๋น ๊ฒฐํจ ์ฌ์ง ๊ธฐ๋ฐ AI ์ธ๊ด ๊ฒ์ฌ์ ์์๋ ํ์ ๋๊ตฌ๋ฅผ ํตํฉํ๊ณ ์๋ ์ฌํ์ตยท์ด์ ๋ฌธ์ยท์์ฒด ์ ๊ฒ์ ๊ฐ์ถ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ ๊ท ์ฌ์ง 240์ฅ์ ์ถ๊ฐํด ์ ํฉ์ฑ์ ๊ฒ์ฆํ๊ณ ๋ถํฌ ๋ณํ(PSI)๋ฅผ ์ ๊ฒํ ๋ค ์ฑ๋ฆฐ์ ๋ชจ๋ธ์ ํ์ตํด ๊ฒ์ฆ MAE๊ฐ 3% ์ด์ ๊ฐ์ ๋ ๋๋ง ์น๊ฒฉ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `docs/qlt03_operations_guide.md ๋ฐ pipeline/qlt03_retrain.py` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์ง์ ๊ฐ๋ฅํ ์ด์ ๊ฐ๋ฅ์ฑ ๋ฐ ๊ตญ๋ฐฉ ๊ตฐ์ ์ ๋ณด์ฒด๊ณ์์ ์ฐ๋ ์ฉ์ด์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ์๋ ์ฌํ์ต ํ์ดํ๋ผ์ธ ์ ์ ์คํ, ์ต์ข
๋ฌธ์ 100% ์๋น\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์ต์ข
์ธ๋๋ฌผ ๊ฒ์ ๋ฐ ํ๋ก์ ํธ ์๋ฃ ๋ฐํ์๋ฃ ์์ฑ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: End-to-End ์ ์ฒด ํ์ดํ๋ผ์ธ ์๋ํ ์คํ ๋ฐ ์ต์ข
๊ฒ์\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์ ๊ท ์ฌ์ง ๊ฐฑ์ , ์ ํฉ์ฑ ๊ฒ์ฆ, ๋ถํฌ ๋ณํ(PSI) ์ ๊ฒ, ์๋ ์ฌํ์ต(์ฑํผ์ธ/์ฑ๋ฆฐ์ ), ๊ฒฐํจ ํ์ ยท์์๋ยท์ ๋น ๋ฑ๊ธ ์ฐ์ถ๊ณผ ๊ฒ์ฌ ๋๊ธฐยท์ฌ์ดฌ์ ๋ถ๋ฆฌ, ์ ์ ๋ถ์ผ์น ํ๋ฅ๊น์ง ํ ๋ฒ์ ์คํํ๋ ํตํฉ ํ์ดํ๋ผ์ธ์ ์์ฑํ๊ณ ์ด์ ๋ฌธ์ ์๋น์จยท์์ฒด ์ ๊ฒยท์ธํฐํ์ด์ค ๋ช
์ธยท๋ธ๋ฆฌํ ์์ฝ์ ์ถ๋ ฅํ๋ค.\"\"\"\n",
"import importlib\n",
"import json\n",
"import sys\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.ensemble import RandomForestClassifier, GradientBoostingRegressor\n",
"from sklearn.linear_model import LinearRegression\n",
"from sklearn.model_selection import cross_val_predict\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ฅ๋น ์ธ๊ด ์ฌ์ง ์์ฑ๊ธฐ (์ค์ ์ฅ๋นยท๋ถ๋ยท์์น ์ฌ์ง์ด ์๋๋ผ ๋์๋ก ๊ทธ๋ฆฐ 64x64 ํฉ์ฑ ์ด๋ฏธ์ง)\n",
"# ๋์ฅ๋ ๊ธ์ ํ๋ฉด์ ๊ท ์ด(crack)ยท๋ถ์(corrosion)ยทํ์(damage)ยท๋์ (leak)๋ฅผ ๊ทธ๋ฆฌ๊ณ , ์กฐ๋ช
ํธ์ฐจ์ ์งํ(mud) ์ค์ผ์ ์๋๋ค.\n",
"# ์ด๋ฏธ์ง์ ํจ๊ป ์ฅ๋น ๊ด๋ฆฌ ๊ธฐ๋ก(์ ์ ์์ฝ)๊ณผ ์ ๋ต ๊ฒฐํจ ๋ง์คํฌยท์์๋ยท์ ๋น ๋ฑ๊ธ์ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"from scipy import ndimage as ndi\n",
"\n",
"SZ = 64\n",
"CLASSES = [\"none\", \"crack\", \"corrosion\", \"damage\", \"leak\"]\n",
"KO_CLS = {\"none\": \"๊ฒฐํจ ์์\", \"crack\": \"๊ท ์ด\", \"corrosion\": \"๋ถ์\", \"damage\": \"ํ์\", \"leak\": \"๋์ \"}\n",
"CLS_W = {\"none\": 0.0, \"crack\": 1.4, \"corrosion\": 1.0, \"damage\": 1.6, \"leak\": 0.6} # ์์๋ ๊ณ์ฐ์ฉ ๊ฒฐํจ ์ ํ ๊ฐ์ค์น\n",
"GRADE_NAME = {1: \"ํ์ฅ์ ๋น\", 2: \"์ผ์ ์ ๋น\", 3: \"์ฐฝ์ ๋น\"}\n",
"YY, XX = np.mgrid[0:SZ, 0:SZ]\n",
"\n",
"\n",
"def grade_of(idx):\n",
" # [TODO : ํ์ ์ค์ต 10] ์ฅ๋น ๊ฒฐํจ ์ฌ์ง ๊ธฐ๋ฐ AI ์ธ๊ด ๊ฒ์ฌ ๋ฐ ์์๋ ์๋ ํ์ ๋๊ตฌ ํตํฉ ์์ฑ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def blob(rng, cy, cx, r, rough=0.35):\n",
" \"\"\"๋ถ๊ท์นํ ์ํ ์์ญ ๋ง์คํฌ.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 10] ์ฅ๋น ๊ฒฐํจ ์ฌ์ง ๊ธฐ๋ฐ AI ์ธ๊ด ๊ฒ์ฌ ๋ฐ ์์๋ ์๋ ํ์ ๋๊ตฌ ํตํฉ ์์ฑ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def draw(rng, cls, sev):\n",
" \"\"\"๊ฒฐํจ ์ ํยท์ฌ๊ฐ๋ sev(0~1)์ ๋ง๋ ์ ๋ต ๋ง์คํฌ์ ์ ์ ๋ณด๋ฅผ ๋ง๋ ๋ค.\"\"\"\n",
" m = np.zeros((SZ, SZ), bool)\n",
" if cls == \"crack\":\n",
" y, x, ang = rng.uniform(12, 52), rng.uniform(12, 52), rng.uniform(0, np.pi)\n",
" for _ in range(int(20 + 40 * sev)):\n",
" ang += rng.normal(0, 0.35); y += np.sin(ang) * 1.2; x += np.cos(ang) * 1.2\n",
" if 0 <= int(y) < SZ and 0 <= int(x) < SZ:\n",
" m[int(y), int(x)] = True\n",
" m = ndi.binary_dilation(m, iterations=1 + int(sev > 0.6))\n",
" elif cls == \"corrosion\":\n",
" for _ in range(int(rng.integers(1, 4))):\n",
" m |= blob(rng, rng.uniform(14, 50), rng.uniform(14, 50), 3 + 9 * sev * rng.uniform(0.6, 1.1))\n",
" elif cls == \"damage\":\n",
" m = blob(rng, rng.uniform(20, 44), rng.uniform(20, 44), 6 + 12 * sev, 0.3)\n",
" elif cls == \"leak\":\n",
" x0, y0 = rng.uniform(16, 48), rng.uniform(8, 24)\n",
" w, ln = 2 + 4 * sev, 12 + 26 * sev\n",
" m = (np.abs(XX - x0 - 0.15 * (YY - y0)) < w * (1 - 0.4 * (YY - y0) / ln)) & (YY > y0) & (YY < y0 + ln)\n",
" return m\n",
"\n",
"\n",
"def make_images(n=600, seed=0, mud=0.0, p_none=0.25, dark=False):\n",
" \"\"\"๊ฐ์ ์ธ๊ด ์ฌ์ง n์ฅ(+์งํ ์ค์ผ mud ๋น์จ)๊ณผ ๊ด๋ฆฌ ๊ธฐ๋กยท์ ๋ต. ๋ฐํ: meta(DataFrame), imgs(n,64,64,3), masks(n,64,64).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" imgs, masks, rows = np.zeros((n, SZ, SZ, 3), np.float32), np.zeros((n, SZ, SZ), bool), []\n",
" for i in range(n):\n",
" cls = \"none\" if rng.random() < p_none else str(rng.choice(CLASSES[1:], p=[0.25, 0.3, 0.25, 0.2]))\n",
" sev = 0.0 if cls == \"none\" else float(rng.beta(1.6, 2.0))\n",
" base = np.array([0.42, 0.46, 0.40]) * (1 + 0.5 * ndi.gaussian_filter(rng.normal(size=(SZ, SZ)), 8)[..., None] * 3)\n",
" img = base + rng.normal(0, 0.012, (SZ, SZ, 3))\n",
" m = draw(rng, cls, sev)\n",
" a = ndi.gaussian_filter(m.astype(float), 0.8)[..., None] # ๊ฒฝ๊ณ๋ฅผ ๋ถ๋๋ฝ๊ฒ ์๋๋ค\n",
" if cls == \"crack\":\n",
" col = img * rng.uniform(0.2, 0.45)\n",
" elif cls == \"corrosion\":\n",
" col = np.array([0.55, 0.30, 0.12]) * rng.uniform(0.75, 1.15) * (0.85 + 0.3 * rng.random((SZ, SZ, 1)))\n",
" elif cls == \"damage\":\n",
" col = np.ones_like(img) * np.array([0.34, 0.34, 0.36]) * (1.0 - 0.45 * sev) * rng.uniform(0.75, 1.0)\n",
" rim = m & ~ndi.binary_erosion(m, iterations=2)\n",
" a = np.maximum(a, 0.0); img = np.where(rim[..., None], 0.7, img)\n",
" elif cls == \"leak\":\n",
" col = np.ones_like(img) * np.array([0.10, 0.09, 0.08]) * rng.uniform(0.8, 2.6)\n",
" else:\n",
" col = img\n",
" img = img * (1 - a) + col * a\n",
" if rng.random() < 0.35: # ๋ฌดํดํ ์ผ๋ฃฉ(๊ฒฐํจ ์๋): ์
์ ๊ฐ์ ๋ฒ์ง\n",
" st = blob(rng, rng.uniform(5, 58), rng.uniform(5, 58), rng.uniform(6, 14), 0.5)\n",
" al = ndi.gaussian_filter(st.astype(float), 2.5)[..., None] * rng.uniform(0.15, 0.4)\n",
" img = img * (1 - al) + np.array([0.50, 0.40, 0.30]) * al\n",
" if rng.random() < 0.3: # ๋ฌดํดํ ๊ธด ์คํฌ๋์น(๋ฐ์ ์ )\n",
" y0, x0, ang = rng.uniform(5, 58), rng.uniform(5, 58), rng.uniform(0, np.pi)\n",
" for t in np.linspace(0, 25, 40):\n",
" yy, xx = int(y0 + t * np.sin(ang)), int(x0 + t * np.cos(ang))\n",
" if 0 <= yy < SZ and 0 <= xx < SZ:\n",
" img[yy, xx] = np.clip(img[yy, xx] * 1.5, 0, 1)\n",
" if mud > 0: # ์งํ ์ค์ผ: ๊ฒฐํจ ์๋ฅผ ๋ฎ์ ์ ์๋ค(์ ๋ต ๋ง์คํฌ๋ ๊ทธ๋๋ก)\n",
" mm = np.zeros((SZ, SZ), bool)\n",
" while mm.mean() < mud:\n",
" mm |= blob(rng, rng.uniform(0, SZ), rng.uniform(0, SZ), rng.uniform(5, 11), 0.5)\n",
" img[mm] = np.array([0.38, 0.30, 0.22]) * (0.85 + 0.3 * rng.random((mm.sum(), 1)))\n",
" gain = rng.uniform(0.3, 0.55) if dark else rng.uniform(0.5, 1.15)\n",
" ang = rng.uniform(0, 2 * np.pi)\n",
" light = gain * (1 + 0.3 * (np.cos(ang) * (XX - 32) + np.sin(ang) * (YY - 32)) / 32)\n",
" imgs[i] = np.clip(img * light[..., None] + rng.normal(0, 0.025, (SZ, SZ, 3)), 0, 1)\n",
" masks[i] = m\n",
" area = float(m.mean())\n",
" depth = {\"none\": 0.0, \"crack\": 0.3 + 3.0 * sev, \"corrosion\": 0.1 + 1.5 * sev, \"damage\": 1.0 + 8.0 * sev, \"leak\": 0.0}[cls] * rng.lognormal(0, 0.1)\n",
" idx = float(np.clip(100 * (1 - np.exp(-(CLS_W[cls] * area * 9 + 0.05 * depth))) + rng.normal(0, 1.5), 0, 100))\n",
" rows.append({\"img_id\": f\"IMG{seed:02d}{i:04d}\", \"cls\": cls, \"sev\": sev, \"area_frac\": area, \"depth_mm\": depth, \"damage_idx\": idx,\n",
" \"grade\": int(grade_of(idx)), \"gain\": gain, \"mud\": mud})\n",
" meta = pd.DataFrame(rows)\n",
" # ์ฅ๋น ๊ด๋ฆฌ ๊ธฐ๋ก(์ ์ ์์ฝ): ์ฌ์ง ํ์ ๊ณผ ๋์กฐํ ๊ฐ\n",
" age = rng.integers(1, 25, n)\n",
" meta[\"day\"] = np.sort(rng.integers(0, 90, n))\n",
" meta[\"equip_type\"] = rng.choice([\"vehicle\", \"generator\", \"comm\", \"trailer\"], n, p=[0.4, 0.2, 0.2, 0.2])\n",
" meta[\"mfg_year\"] = 2026 - age\n",
" meta[\"op_hours\"] = np.clip(rng.normal(180 * age, 400, n) + 8 * meta[\"damage_idx\"], 10, 20000)\n",
" meta[\"env\"] = rng.choice([\"coastal\", \"inland\"], n, p=[0.3, 0.7])\n",
" reg = np.clip(meta[\"grade\"] + rng.choice([-1, 0, 0, 0, 0, 0, 1, 2], n), 1, 3) # ์ฅ๋น ๊ด๋ฆฌ ๊ธฐ๋ก์ ์ง์ ์ ๋น ๋ฑ๊ธ\n",
" meta[\"reg_grade\"] = reg\n",
" meta[\"emergency\"] = rng.random(n) < 0.04 # ๊ธด๊ธ ์ ๊ฒ ์์ฒญ(ํ์ง ๋ถ๋์ด์ด๋ ๋ณด์กด)\n",
" return meta, imgs, masks\n",
"# ---- ์ ์ฒ๋ฆฌ(์กฐ๋ช
๋ณด์ ยทํ์คํ)์ ๊ฒฐํจ ํ๋ณด ๋ถํ , ํน์ง ์ถ์ถ (cv2 ๋์ numpyยทscipy ์ฌ์ฉ) ----\n",
"def correct(img):\n",
" \"\"\"์กฐ๋ช
๋ณด์ : ๋ฐ๊ธฐ์ ํ๋ฉด(์กฐ๋ช
๊ธฐ์ธ๊ธฐ)์ ์ ํฉํด ๋๋๋ค. ์ด๋์ด ๊ฒฐํจ ํ์๋ ์ ์ธํ๊ณ ๋ ๋ฒ ๋ฐ๋ณต ์ ํฉํ๋ค. ๋ฐํ: ๋ณด์ ๋ฐ๊ธฐ(์ ์ ๋ฉด ์ฝ 1), ์ ๋น์จ.\"\"\"\n",
" g = img.mean(axis=2)\n",
" A = np.c_[np.ones(SZ * SZ), XX.ravel() / SZ, YY.ravel() / SZ]\n",
" keep = np.ones(SZ * SZ, bool)\n",
" for _ in range(3):\n",
" coef, *_ = np.linalg.lstsq(A[keep], g.ravel()[keep], rcond=None)\n",
" plane = (A @ coef).reshape(SZ, SZ)\n",
" keep = (np.abs(g - plane) < 0.08 * np.maximum(plane, 1e-3)).ravel()\n",
" if keep.sum() < 200:\n",
" keep[:] = True\n",
" c = g / np.maximum(plane, 1e-3)\n",
" chroma = img[..., 0] / (img.sum(axis=2) + 1e-6)\n",
" return c, chroma\n",
"\n",
"\n",
"SEGP = {\"dark\": 0.7, \"rust\": 0.5, \"rim\": 1.45} # ๋ถํ ์๊ณ๊ฐ(์ด๋์ ๋ณด์ ๋ฐ๊ธฐ, ๋ถ์ ๋น์จ, ๋ฐ์ ํ
๋๋ฆฌ): ์ฌ๋ณด์ ๋์\n",
"\n",
"\n",
"def segment(img):\n",
" \"\"\"๊ฒฐํจ ํ๋ณด ๋ง์คํฌ: ์ด๋์ด ์์ญ(๊ท ์ดยท๋์ ยทํ์) + ๋ถ์ ๊ฐ์ ์์ญ(๋ถ์) + ๋ฐ์ ํ
๋๋ฆฌ(ํ์).\"\"\"\n",
" c, chroma = correct(img)\n",
" dark = c < SEGP[\"dark\"]\n",
" rust = (chroma > SEGP[\"rust\"]) & (c > 0.45)\n",
" rim = c > SEGP[\"rim\"]\n",
" m = dark | rust | ndi.binary_dilation(rim, iterations=1) & ndi.binary_dilation(dark, iterations=3)\n",
" m = ndi.binary_opening(m, structure=np.ones((2, 2))) if m.sum() > 12 else m\n",
" return m, c, chroma, dark, rust, rim\n",
"\n",
"\n",
"def extract(img):\n",
" \"\"\"๋ถํ ๊ฒฐ๊ณผ์ ์ยท๋ชจ์์์ 24๊ฐ ํน์ง์ ๋ง๋ ๋ค.\"\"\"\n",
" m, c, chroma, dark, rust, rim = segment(img)\n",
" lab, n = ndi.label(m)\n",
" area = float(m.sum())\n",
" if n:\n",
" sizes = ndi.sum(m, lab, range(1, n + 1))\n",
" k = int(np.argmax(sizes)) + 1\n",
" sl = ndi.find_objects(lab)[k - 1]\n",
" h, w = sl[0].stop - sl[0].start, sl[1].stop - sl[1].start\n",
" big = float(sizes.max())\n",
" elong, fill = max(h, w) / max(min(h, w), 1), big / (h * w)\n",
" else:\n",
" big, elong, fill = 0.0, 1.0, 0.0\n",
" gy, gx = np.gradient(c)\n",
" mag = np.hypot(gx, gy)\n",
" inside = m.sum() > 0\n",
" return {\"area_px\": area, \"area_frac\": area / SZ ** 2, \"n_comp\": float(n), \"big_px\": big, \"elong\": elong, \"fill\": fill,\n",
" \"dark_frac\": float(dark.mean()), \"rust_frac\": float(rust.mean()), \"rim_frac\": float(rim.mean()),\n",
" \"c_mean\": float(c.mean()), \"c_std\": float(c.std()), \"c_min\": float(c.min()), \"edge_mean\": float(mag.mean()), \"edge_p95\": float(np.quantile(mag, 0.95)),\n",
" \"chroma_mean\": float(chroma.mean()), \"chroma_max\": float(chroma.max()),\n",
" \"in_dark\": float(c[m].mean()) if inside else 1.0, \"in_chroma\": float(chroma[m].mean()) if inside else float(chroma.mean()),\n",
" \"ring_contrast\": float(c[ndi.binary_dilation(m, iterations=3) & ~m].mean() - c[m].mean()) if inside and (~m).any() else 0.0,\n",
" \"thin\": float(1 - fill), \"gain\": float(img.mean()), \"sat\": float((img.max(axis=2) - img.min(axis=2)).mean()),\n",
" \"cy\": float(ndi.center_of_mass(m)[0] / SZ) if inside else 0.5, \"cx\": float(ndi.center_of_mass(m)[1] / SZ) if inside else 0.5}\n",
"\n",
"\n",
"def feature_table(imgs):\n",
" return pd.DataFrame([extract(im) for im in imgs])\n",
"# ---- ํ์ ํ์ดํ๋ผ์ธ: ๊ฒฐํจ ์ ํ ๋ถ๋ฅ + ์์๋ ํ๊ท(๋ถ๋ฅ ํ๋ฅ ํฌํจ) -> ์ ๋น ๋ฑ๊ธ ----\n",
"def fit_pipeline(meta, F):\n",
" \"\"\"๊ฒฐํจ ์ ํ ๋ถ๋ฅ๊ธฐ์ ์์๋ ํ๊ท๊ธฐ๋ฅผ ํ์ตํ๋ค. ํ๊ท๊ธฐ ์
๋ ฅ์ ๋ถ๋ฅ ํ๋ฅ ์ ๊ต์ฐจ ์์ธก์ผ๋ก ๋ง๋ค์ด ๋์ค์ ๋ง๋๋ค.\"\"\"\n",
" clf = RandomForestClassifier(n_estimators=150, random_state=0, n_jobs=1)\n",
" P = cross_val_predict(clf, F, meta[\"cls\"], cv=4, method=\"predict_proba\")\n",
" clf.fit(F, meta[\"cls\"])\n",
" reg = GradientBoostingRegressor(n_estimators=200, max_depth=3, learning_rate=0.06, random_state=0).fit(np.c_[F.values, P], meta[\"damage_idx\"])\n",
" return {\"clf\": clf, \"reg\": reg, \"cols\": list(F.columns)}\n",
"\n",
"\n",
"def predict_pipeline(pl, F):\n",
" X = F[pl[\"cols\"]]\n",
" P = pl[\"clf\"].predict_proba(X)\n",
" idx = np.clip(pl[\"reg\"].predict(np.c_[X.values, P]), 0, 100)\n",
" return pl[\"clf\"].classes_[P.argmax(axis=1)], P, idx, P.max(axis=1)\n",
"# ---- ์ฌ์ง ํ์ง ๊ฒ์ดํธ: ์งํยท์ค์ผ ๊ฐ๋ฆผ ์ถ์ ๊ณผ ํ์ ์ ๋ขฐ๋ ์ง์ ----\n",
"def mud_est(img):\n",
" \"\"\"์งํ(๊ฐ์, ์ฑ๋ ๋ฎ์ ๋ถ์ ๊ธฐ) ํ์ ๋น์จ์ ์ถ์ ํ๋ค. 3x3 ํ๊ท ์ผ๋ก ์ก์์ ์ค์ธ ๋ค ์ ๋น์จ๋ก ํ๋จํ๋ฉฐ, ์ค์ ๊ฐ๋ฆผ ๋น์จ๋ณด๋ค ์๊ฒ ๋์ค์ง๋ง ์์๋ ์ ์ง๋๋ค.\"\"\"\n",
" sm = ndi.uniform_filter(img, size=(3, 3, 1))\n",
" s = sm.sum(axis=2) + 1e-6\n",
" rr, gg = sm[..., 0] / s, sm[..., 1] / s\n",
" return float(((rr > 0.385) & (rr < 0.46) & (gg < 0.325)).mean())\n",
"\n",
"\n",
"def confidence(P, idx, F, mud):\n",
" \"\"\"ํ์ ์ ๋ขฐ๋ ์ง์ 0~100 = 50 x ๋ถ๋ฅ ํ๋ฅ ์ต๋๊ฐ + 35 x ๋ฑ๊ธ ๊ฒฝ๊ณ๊น์ง ๊ฑฐ๋ฆฌ(8์ ์์ ํฌํ) + 15 x ์ฌ์ง ํ์ง. ์ค์ผ ์์ฌ(mud ์ถ์ 1% ์ด์)์ด๋ฉด ์ํ 75๋ก ์ ํํ๋ค.\"\"\"\n",
" bd = np.minimum(np.abs(idx - 35), np.abs(idx - 65))\n",
" qual = np.clip(1.0 - np.clip(0.55 - F[\"gain\"].values, 0, None) * 2 - np.clip(0.9 - F[\"c_mean\"].values, 0, None), 0, 1)\n",
" c = 50 * P.max(axis=1) + 35 * np.clip(bd / 8, 0, 1) + 15 * qual\n",
" return np.where(np.asarray(mud) >= 0.01, np.minimum(c, 75.0), c)\n",
"\n",
"libs = [\"numpy\", \"pandas\", \"scipy\", \"sklearn\", \"matplotlib\"]\n",
"env_ok = sum(importlib.util.find_spec(m) is not None for m in libs)\n",
"print(f\"[0] ํ๊ฒฝ ์ ๊ฒ: Python {sys.version_info.major}.{sys.version_info.minor}, ํ์ ๋ชจ๋ {env_ok}/{len(libs)}\")\n",
"\n",
"# ---------------- 1) ์ ๊ท ์ฌ์ง ๊ฐฑ์ ยท์ ํฉ์ฑ ๊ฒ์ฆ(๋ฏธ์
1~2 ์์ฝ) ----------------\n",
"m_old, i_old, k_old = make_images(600, seed=1) # ๊ธฐ์กด 600์ฅ\n",
"parts = [make_images(80, seed=80, dark=True), make_images(80, seed=81, mud=0.05), make_images(80, seed=82)]\n",
"m_new = pd.concat([p[0] for p in parts], ignore_index=True); i_new = np.concatenate([p[1] for p in parts]); k_new = np.concatenate([p[2] for p in parts])\n",
"m_new[\"img_id\"] = [\"NEW\" + x[3:] for x in m_new[\"img_id\"]] # ์ ๊ท ์ฌ์ง ID ๊ตฌ๋ถ\n",
"meta = pd.concat([m_old, m_new], ignore_index=True); imgs = np.concatenate([i_old, i_new]); masks = np.concatenate([k_old, k_new])\n",
"chk = {\"์ฌ์ง ID ์ ์ผ\": not meta[\"img_id\"].duplicated().any(), \"๊ฒฐ์ธก 0(์ ์ยท๋ผ๋ฒจ)\": int(meta[[\"equip_type\", \"mfg_year\", \"op_hours\", \"env\", \"reg_grade\"]].isna().sum().sum()) == 0,\n",
" \"ํ์ ๋ฒ์ 0~1\": bool((imgs.min() >= 0) and (imgs.max() <= 1)), \"ํด์๋ 64x64x3\": imgs.shape[1:] == (64, 64, 3), \"์ ์กฐ์ฐ๋ ๊ฐ๋ฅ ๋ฒ์\": bool(meta[\"mfg_year\"].between(1990, 2026).all()),\n",
" \"๋ง์คํฌ ๋ฉด์ = ๋ผ๋ฒจ ๋ฉด์ \": bool(np.allclose(masks.mean(axis=(1, 2)), meta[\"area_frac\"], atol=1e-6)), \"์์๋-๋ฑ๊ธ ์ผ์น\": bool((grade_of(meta[\"damage_idx\"]) == meta[\"grade\"]).all())}\n",
"print(f\"[1] ์ ๊ท 240์ฅ ํฌํจ {len(meta)}์ฅ ์ ํฉ์ฑ ๊ฒ์ฆ {sum(chk.values())}/{len(chk)} ํต๊ณผ, ๊ธด๊ธ ์ ๊ฒ ์ฌ์ง {int(meta['emergency'].sum())}์ฅ ๋ณด์กด\")\n",
"F = feature_table(imgs)\n",
"F[\"mud\"] = [mud_est(x) for x in imgs]\n",
"n_old = len(m_old)\n",
"\n",
"# ---------------- 2) ๋ถํฌ ๋ณํ ์ ๊ฒ(PSI) ๋ฐ ์๋ ์ฌํ์ต ----------------\n",
"def psi(a, b, bins=8):\n",
" q = np.unique(np.quantile(a, np.linspace(0, 1, bins + 1)))\n",
" q[0], q[-1] = -np.inf, np.inf\n",
" pa, pb = np.histogram(a, q)[0] / len(a) + 1e-4, np.histogram(b, q)[0] / len(b) + 1e-4\n",
" return float(np.sum((pa - pb) * np.log(pa / pb)))\n",
"\n",
"\n",
"drift = {c: psi(F[c][:n_old], F[c][n_old:]) for c in [\"gain\", \"c_mean\", \"c_std\", \"rust_frac\", \"area_frac\", \"mud\"]}\n",
"worst = max(drift, key=drift.get)\n",
"print(\"[2] ๋ถํฌ ๋ณํ PSI(๊ธฐ์กด 600 vs ์ ๊ท 240):\", \", \".join(f\"{k} {v:.2f}\" for k, v in drift.items()), f\"-> ๊ฐ์ฅ ํฐ ๋ณํ {worst} (0.2 ์ด์์ด๋ฉด ์ฌํ์ต ๊ฒํ )\")\n",
"rng = np.random.default_rng(0)\n",
"new_idx = rng.permutation(np.arange(n_old, len(meta)))\n",
"tr_new, hold = new_idx[:160], new_idx[160:] # ์ ๊ท ์ค 160์ฅ์ ์ฌํ์ต์ฉ, 80์ฅ์ ๊ฒ์ฆ์ฉ\n",
"cols = [c for c in F.columns if c != \"mud\"]\n",
"\n",
"\n",
"def fit_on(ix):\n",
" return fit_pipeline(meta.iloc[ix].reset_index(drop=True), F.iloc[ix][cols].reset_index(drop=True))\n",
"\n",
"\n",
"def score(pl, ix):\n",
" cls, P, idx, _ = predict_pipeline(pl, F.iloc[ix][cols])\n",
" y = meta[\"damage_idx\"].values[ix]\n",
" return float(np.mean(np.abs(y - idx))), float(np.mean(grade_of(idx) == meta[\"grade\"].values[ix]))\n",
"\n",
"\n",
"champ = fit_on(np.arange(n_old))\n",
"chall = fit_on(np.r_[np.arange(n_old), tr_new])\n",
"a, b = score(champ, hold), score(chall, hold)\n",
"promote = (a[0] - b[0]) / a[0] >= 0.03\n",
"drifted = max(drift.values()) >= 0.2\n",
"print(f\" ๊ฒ์ฆ 80์ฅ(์ ๊ท ๋ถํฌ): ์ฑํผ์ธ MAE {a[0]:.2f}ยท๋ฑ๊ธ ์ผ์น {a[1]:.1%} vs ์ฑ๋ฆฐ์ MAE {b[0]:.2f}ยท๋ฑ๊ธ ์ผ์น {b[1]:.1%} -> {'์ฑ๋ฆฐ์ ์น๊ฒฉ' if promote else '๊ธฐ์กด ๋ชจ๋ธ ์ ์ง'}(MAE 3% ์ด์ ๊ฐ์ ์ผ ๋๋ง ์น๊ฒฉ)\")\n",
"model = chall if promote else champ\n",
"\n",
"# ---------------- 3) ์ ๊ท ์ฌ์ง ํ์ ๊ณผ ๊ฒ์ฌ ๋๊ธฐ ๋ถ๋ฆฌ ----------------\n",
"Xn = F.iloc[n_old:][cols].reset_index(drop=True)\n",
"cls, P, idx, _ = predict_pipeline(model, Xn)\n",
"mud = F[\"mud\"].values[n_old:]\n",
"conf = confidence(P, idx, Xn, mud)\n",
"res = pd.DataFrame({\"์ฌ์ง\": meta[\"img_id\"].values[n_old:], \"ํ์ ์ ํ\": [KO_CLS[c] for c in cls], \"์์๋\": idx, \"AI ๋ฑ๊ธ\": grade_of(idx), \"์ ๋ขฐ๋\": conf, \"์ค์ผ\": mud, \"๊ธฐ๋ก ๋ฑ๊ธ\": meta[\"reg_grade\"].values[n_old:], \"์ค์ ๋ฑ๊ธ\": meta[\"grade\"].values[n_old:]})\n",
"res[\"์ฒ๋ฆฌ\"] = np.select([(mud >= 0.03) | (Xn[\"gain\"].values < 0.17), conf >= 80], [\"์ฌ์ดฌ์ ์๊ตฌ\", \"๊ฒ์ฌ ๋๊ธฐ(๊ฐ์ด ์น์ธ ํ๋ณด)\"], \"๊ฒ์ฌ ๋๊ธฐ(์ ๋ฐ ํ๋
)\")\n",
"res[\"์ ์ ์ ํธ\"] = np.abs(res[\"AI ๋ฑ๊ธ\"] - res[\"๊ธฐ๋ก ๋ฑ๊ธ\"]) >= 2\n",
"print(f\"\\n[3] ์ ๊ท {len(res)}์ฅ ํ์ ๊ฒฐ๊ณผ [๊ต์ก์ฉ๊ฐ์์๋ฃ] (์์ 6๊ฑด)\")\n",
"print(res.head(6)[[\"์ฌ์ง\", \"ํ์ ์ ํ\", \"์์๋\", \"AI ๋ฑ๊ธ\", \"์ ๋ขฐ๋\", \"์ค์ผ\", \"์ฒ๋ฆฌ\"]].round(2).to_string(index=False))\n",
"vc = res[\"์ฒ๋ฆฌ\"].value_counts()\n",
"print(\" ์ฒ๋ฆฌ ๊ตฌ๋ถ:\", vc.to_dict(), \"/ ์๋ ํ์ 0๊ฑด(๋ชจ๋ ํ์ ์ ๊ฒ์ฌ๊ด ํ์ธ๊ณผ ์ ๋น๊ฐ๋
๊ด ๊ฒฐ์ฌ ํ์)\")\n",
"auto = res[\"์ฒ๋ฆฌ\"].str.contains(\"๊ฐ์ด\")\n",
"print(f\" ๊ฐ์ด ์น์ธ ํ๋ณด {int(auto.sum())}์ฅ์ ๋ฑ๊ธ ์ ๋ต ์ผ์น์จ {(res['AI ๋ฑ๊ธ'] == res['์ค์ ๋ฑ๊ธ'])[auto].mean():.1%}, ์ ์ฒด ์ผ์น์จ {(res['AI ๋ฑ๊ธ'] == res['์ค์ ๋ฑ๊ธ']).mean():.1%}\")\n",
"print(f\" ์ ์ ํ๋ฅ: AI ๋ฑ๊ธ๊ณผ ๊ด๋ฆฌ ๊ธฐ๋ก ๋ฑ๊ธ์ด 2๋จ๊ณ ์ด์ ๋ค๋ฅธ {int(res['์ ์ ์ ํธ'].sum())}์ฅ์ ์ ์ ์์ ์์ฒญ ํ๋ณด(์๋ ์์ ๊ธ์ง, ๋ด๋น์ ํ์ธ)\")\n",
"\n",
"# ---------------- 4) ์ด์ ๋ฌธ์ ์๋น์จยท์ธํฐํ์ด์ค ๋ช
์ธ ----------------\n",
"docs = {\"๋ฐ์ดํฐ ์ฌ์ ยท์คํค๋ง(M1)\": 24, \"์ ์ ๊ท์น 11์ข
(M2)\": 11, \"BaselineยทEDA(M3)\": 8, \"๋ถํ ยท์์๋ ๋ชจ๋ธ(M4)\": 24, \"๋ฑ๊ธยท์ ๋ขฐ๋ ์์ง(M5)\": 3, \"ํ์ ์ธํฐํ์ด์คยท์ค๋ช
(M6)\": 5,\n",
" \"์น์ธ ์ ์ฐจยท๊ถํ(M7)\": 4, \"์ค์ผ ์คํธ๋ ์คยทํ๊ณ์น(M8)\": 7, \"AARยทROI(M9)\": 4, \"์ด์ยท์ฌํ์ต ์ ์ฐจ(M10)\": 6, \"์ฅ์ ๋์\": 5, \"์ธํฐํ์ด์ค ๊ฐ์ด๋\": 4}\n",
"done = sum(v > 0 for v in docs.values())\n",
"iface = {\"input\": {\"photo\": [\"img_id:str\", \"image:64x64x3 float\", \"equip_type:str\", \"reg_grade:int\"], \"format\": \"image+json\"},\n",
" \"output\": {\"result\": [\"img_id\", \"defect\", \"damage_idx\", \"grade\", \"confidence\", \"mud\", \"state\"], \"approval\": \"PENDING_ONLY\"}, \"auth\": \"role-based(๊ฐ์)\", \"note\": \"์์ ๊ตฐ์์ ๋ณด์ฒด๊ณ๋ ๊ฐ์ ์ธํฐํ์ด์ค๋ก๋ง ์ ์\"}\n",
"print(f\"\\n[4] ์ด์ ๋ฌธ์ {len(docs)}๊ฐ ํญ๋ชฉ ์ค ์์ฑ ์๋ฃ {done}๊ฐ ({done / len(docs):.0%}) / ์ธํฐํ์ด์ค ๋ช
์ธ(JSON):\")\n",
"print(\" \", json.dumps(iface, ensure_ascii=False))\n",
"\n",
"# ---------------- 5) ์์ฒด ์ ๊ฒยท๋ธ๋ฆฌํ ----------------\n",
"ev_ix = np.arange(n_old, len(meta))\n",
"y = meta[\"damage_idx\"].values[ev_ix]\n",
"fin_mae = float(np.mean(np.abs(y - idx)))\n",
"big = y >= 10\n",
"fin_mape = float(np.mean(np.abs(y[big] - idx[big]) / y[big]) * 100)\n",
"base = LinearRegression().fit(F.iloc[:n_old][[\"area_px\"]], meta[\"damage_idx\"][:n_old])\n",
"base_mae = float(np.mean(np.abs(y - np.clip(base.predict(F.iloc[n_old:][[\"area_px\"]]), 0, 100))))\n",
"checks = [(\"ํ๊ฒฝ ์ ๊ฒ ํต๊ณผ\", env_ok == len(libs)), (\"๋ฐ์ดํฐ ์ ํฉ์ฑ ์ ํญ๋ชฉ ํต๊ณผ\", all(chk.values())), (\"์ฌํ์ต ํ์ดํ๋ผ์ธ ์ ์ ์คํ(์ฑํผ์ธ/์ฑ๋ฆฐ์ ํ์ ์ถ๋ ฅ)\", a[0] > 0 and b[0] > 0),\n",
" (\"์์๋ MAPE 25% ์ดํ(์ ์ 10 ์ด์)\", fin_mape <= 25), (\"๋ฉด์ ๊ธฐ์ค ๋ชจ๋ธ ๋๋น MAE 20% ์ด์ ๊ฐ์ \", fin_mae <= 0.8 * base_mae),\n",
" (\"์๋ ํ์ 0๊ฑด(๊ฒ์ฌ ๋๊ธฐยท์ฌ์ดฌ์ ๋ถ๋ฆฌ)\", bool(res[\"์ฒ๋ฆฌ\"].str.contains(\"๊ฒ์ฌ ๋๊ธฐ|์ฌ์ดฌ์\").all())), (\"์ค์ผ ์์ฌ ์ฌ์ง ์ ๋ถ ์ฌ์ดฌ์ ๋๋ ์ ๋ฐ ํ๋
์ผ๋ก ๋ถ๋ฆฌ\", bool(res.loc[res[\"์ค์ผ\"] >= 0.01, \"์ฒ๋ฆฌ\"].ne(\"๊ฒ์ฌ ๋๊ธฐ(๊ฐ์ด ์น์ธ ํ๋ณด)\").all())),\n",
" (\"์ด์ ๋ฌธ์ 100% ์๋น\", done == len(docs))]\n",
"print(\"\\n[5] ์์ฒด ์ ๊ฒ\")\n",
"for n, ok in checks:\n",
" print(f\" [{'PASS' if ok else 'FAIL'}] {n}\")\n",
"print(\"\\n[6] ๋ธ๋ฆฌํ ์์ฝ\")\n",
"print(f\" - ์ ๊ท ์ฌ์ง 240์ฅ์ผ๋ก ๋ถํฌ ๋ณํ๋ฅผ ์ ๊ฒ({worst} PSI {drift[worst]:.2f}, {'์ฌํ์ต ๊ฒํ ์์ค' if drifted else '๊ฒฝ๋ฏธ'})ํ๊ณ ์ฌํ์ต ํ๋จ์ ์๋ ์ํํ๋ค({'์ฑ๋ฆฐ์ ์น๊ฒฉ' if promote else '๊ธฐ์กด ๋ชจ๋ธ ์ ์ง'}).\")\n",
"print(f\" - ์ ๊ท ์ฌ์ง ์์๋ MAE {base_mae:.1f}(๋ฉด์ ๊ธฐ์ค) -> {fin_mae:.1f}, MAPE {fin_mape:.1f}%. ์ฒ๋ฆฌ ๊ตฌ๋ถ: \" + \", \".join(f\"{k} {v}์ฅ\" for k, v in vc.items()) + \".\")\n",
"print(\" - ์ต์ข
์ ๋น ๋ฑ๊ธ์ ๊ฒ์ฌ๊ดยท์ ๋น๊ฐ๋
๊ด ๊ฒฐ์ฌ๋ก ํ์ ํ๋ฉฐ AI ํ์ ์ ์๋ ํ์ ๋์ง ์๋๋ค. ์ค์ผ ์ฌ์ง์ ์ฌ์ดฌ์์ ์๊ตฌํ๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(12, 4))\n",
"ax[0].bar(list(drift), list(drift.values())); ax[0].axhline(0.2, c=\"r\", ls=\"--\"); ax[0].set_title(\"Feature drift (PSI), old vs new photos\"); ax[0].tick_params(axis=\"x\", rotation=30)\n",
"ax[1].scatter(y, idx, s=8, c=np.where(res[\"์ฒ๋ฆฌ\"] == \"์ฌ์ดฌ์ ์๊ตฌ\", \"r\", \"g\")); ax[1].plot([0, 100], [0, 100], \"k--\"); ax[1].set_title(\"New photos: damage index true vs predicted (red = retake)\")\n",
"plt.suptitle(\"QLT03 integrated pipeline (synthetic photos)\"); plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
10 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[0] ํ๊ฒฝ ์ ๊ฒ: Python 3.11, ํ์ ๋ชจ๋ 5/5\n",
"[1] ์ ๊ท 240์ฅ ํฌํจ 840์ฅ ์ ํฉ์ฑ ๊ฒ์ฆ 7/7 ํต๊ณผ, ๊ธด๊ธ ์ ๊ฒ ์ฌ์ง 38์ฅ ๋ณด์กด\n",
"[2] ๋ถํฌ ๋ณํ PSI(๊ธฐ์กด 600 vs ์ ๊ท 240): gain 0.56, c_mean 0.10, c_std 0.26, rust_frac 0.39, area_frac 0.07, mud 0.79 -> ๊ฐ์ฅ ํฐ ๋ณํ mud (0.2 ์ด์์ด๋ฉด ์ฌํ์ต ๊ฒํ )\n",
" ๊ฒ์ฆ 80์ฅ(์ ๊ท ๋ถํฌ): ์ฑํผ์ธ MAE 8.79ยท๋ฑ๊ธ ์ผ์น 82.5% vs ์ฑ๋ฆฐ์ MAE 6.86ยท๋ฑ๊ธ ์ผ์น 86.2% -> ์ฑ๋ฆฐ์ ์น๊ฒฉ(MAE 3% ์ด์ ๊ฐ์ ์ผ ๋๋ง ์น๊ฒฉ)\n",
"\n",
"[3] ์ ๊ท 240์ฅ ํ์ ๊ฒฐ๊ณผ [๊ต์ก์ฉ๊ฐ์์๋ฃ] (์์ 6๊ฑด)\n",
" ์ฌ์ง ํ์ ์ ํ ์์๋ AI ๋ฑ๊ธ ์ ๋ขฐ๋ ์ค์ผ ์ฒ๋ฆฌ\n",
"NEW800000 ๊ท ์ด 34.85 1 53.85 0.0 ์ฌ์ดฌ์ ์๊ตฌ\n",
"NEW800001 ๋ถ์ 30.58 1 72.79 0.0 ๊ฒ์ฌ ๋๊ธฐ(์ ๋ฐ ํ๋
)\n",
"NEW800002 ๊ฒฐํจ ์์ 1.25 1 87.85 0.0 ๊ฒ์ฌ ๋๊ธฐ(๊ฐ์ด ์น์ธ ํ๋ณด)\n",
"NEW800003 ๊ฒฐํจ ์์ 9.30 1 86.00 0.0 ๊ฒ์ฌ ๋๊ธฐ(๊ฐ์ด ์น์ธ ํ๋ณด)\n",
"NEW800004 ๋ถ์ 41.78 2 81.09 0.0 ์ฌ์ดฌ์ ์๊ตฌ\n",
"NEW800005 ๋ถ์ 36.30 2 58.39 0.0 ์ฌ์ดฌ์ ์๊ตฌ\n",
" ์ฒ๋ฆฌ ๊ตฌ๋ถ: {'๊ฒ์ฌ ๋๊ธฐ(๊ฐ์ด ์น์ธ ํ๋ณด)': 112, '๊ฒ์ฌ ๋๊ธฐ(์ ๋ฐ ํ๋
)': 86, '์ฌ์ดฌ์ ์๊ตฌ': 42} / ์๋ ํ์ 0๊ฑด(๋ชจ๋ ํ์ ์ ๊ฒ์ฌ๊ด ํ์ธ๊ณผ ์ ๋น๊ฐ๋
๊ด ๊ฒฐ์ฌ ํ์)\n",
" ๊ฐ์ด ์น์ธ ํ๋ณด 112์ฅ์ ๋ฑ๊ธ ์ ๋ต ์ผ์น์จ 99.1%, ์ ์ฒด ์ผ์น์จ 90.4%\n",
" ์ ์ ํ๋ฅ: AI ๋ฑ๊ธ๊ณผ ๊ด๋ฆฌ ๊ธฐ๋ก ๋ฑ๊ธ์ด 2๋จ๊ณ ์ด์ ๋ค๋ฅธ 19์ฅ์ ์ ์ ์์ ์์ฒญ ํ๋ณด(์๋ ์์ ๊ธ์ง, ๋ด๋น์ ํ์ธ)\n",
"\n",
"[4] ์ด์ ๋ฌธ์ 12๊ฐ ํญ๋ชฉ ์ค ์์ฑ ์๋ฃ 12๊ฐ (100%) / ์ธํฐํ์ด์ค ๋ช
์ธ(JSON):\n",
" {\"input\": {\"photo\": [\"img_id:str\", \"image:64x64x3 float\", \"equip_type:str\", \"reg_grade:int\"], \"format\": \"image+json\"}, \"output\": {\"result\": [\"img_id\", \"defect\", \"damage_idx\", \"grade\", \"confidence\", \"mud\", \"state\"], \"approval\": \"PENDING_ONLY\"}, \"auth\": \"role-based(๊ฐ์)\", \"note\": \"์์ ๊ตฐ์์ ๋ณด์ฒด๊ณ๋ ๊ฐ์ ์ธํฐํ์ด์ค๋ก๋ง ์ ์\"}\n",
"\n",
"[5] ์์ฒด ์ ๊ฒ\n",
" [PASS] ํ๊ฒฝ ์ ๊ฒ ํต๊ณผ\n",
" [PASS] ๋ฐ์ดํฐ ์ ํฉ์ฑ ์ ํญ๋ชฉ ํต๊ณผ\n",
" [PASS] ์ฌํ์ต ํ์ดํ๋ผ์ธ ์ ์ ์คํ(์ฑํผ์ธ/์ฑ๋ฆฐ์ ํ์ ์ถ๋ ฅ)\n",
" [PASS] ์์๋ MAPE 25% ์ดํ(์ ์ 10 ์ด์)\n",
" [PASS] ๋ฉด์ ๊ธฐ์ค ๋ชจ๋ธ ๋๋น MAE 20% ์ด์ ๊ฐ์ \n",
" [PASS] ์๋ ํ์ 0๊ฑด(๊ฒ์ฌ ๋๊ธฐยท์ฌ์ดฌ์ ๋ถ๋ฆฌ)\n",
" [PASS] ์ค์ผ ์์ฌ ์ฌ์ง ์ ๋ถ ์ฌ์ดฌ์ ๋๋ ์ ๋ฐ ํ๋
์ผ๋ก ๋ถ๋ฆฌ\n",
" [PASS] ์ด์ ๋ฌธ์ 100% ์๋น\n",
"\n",
"[6] ๋ธ๋ฆฌํ ์์ฝ\n",
" - ์ ๊ท ์ฌ์ง 240์ฅ์ผ๋ก ๋ถํฌ ๋ณํ๋ฅผ ์ ๊ฒ(mud PSI 0.79, ์ฌํ์ต ๊ฒํ ์์ค)ํ๊ณ ์ฌํ์ต ํ๋จ์ ์๋ ์ํํ๋ค(์ฑ๋ฆฐ์ ์น๊ฒฉ).\n",
" - ์ ๊ท ์ฌ์ง ์์๋ MAE 13.0(๋ฉด์ ๊ธฐ์ค) -> 4.5, MAPE 10.6%. ์ฒ๋ฆฌ ๊ตฌ๋ถ: ๊ฒ์ฌ ๋๊ธฐ(๊ฐ์ด ์น์ธ ํ๋ณด) 112์ฅ, ๊ฒ์ฌ ๋๊ธฐ(์ ๋ฐ ํ๋
) 86์ฅ, ์ฌ์ดฌ์ ์๊ตฌ 42์ฅ.\n",
" - ์ต์ข
์ ๋น ๋ฑ๊ธ์ ๊ฒ์ฌ๊ดยท์ ๋น๊ฐ๋
๊ด ๊ฒฐ์ฌ๋ก ํ์ ํ๋ฉฐ AI ํ์ ์ ์๋ ํ์ ๋์ง ์๋๋ค. ์ค์ผ ์ฌ์ง์ ์ฌ์ดฌ์์ ์๊ตฌํ๋ค.\n",
"```\n",
" \n"
]
}
],
"metadata": {
"colab": {
"provenance": []
},
"kernelspec": {
"display_name": "Python 3",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 0
}