File size: 17,269 Bytes
fe1431a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109eca5
 
 
 
fe1431a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "91bd5e5c",
   "metadata": {},
   "source": [
    "# KNPS 2026 λΆˆλ²•μ‹œμ„€λ¬Ό 변화탐지 베이슀라인 제좜 (UNet-ResNet18, 6채널)\n",
    "\n",
    "같은 μœ„μΉ˜μ˜ μ „(pre)/ν›„(post) μœ„μ„± RGB 패치(256x256) ν•œ μŒμ—μ„œ **증좕(new_building)** κ³Ό\n",
    "**벌λͺ©(tree_removal)** μ˜μ—­μ„ 각각 폴리곀 λͺ©λ‘μœΌλ‘œ 좜λ ₯ν•˜λŠ” μ°Έκ³  κ΅¬ν˜„μž…λ‹ˆλ‹€. μ„±λŠ₯ 기쀀선이 μ•„λ‹ˆλΌ\n",
    "**μž…λ ₯μ—μ„œ 제좜 νŒŒμΌκΉŒμ§€μ˜ 흐름과 제좜 κ·œκ²©μ„ ν™•μΈν•˜λŠ” μ΅œμ†Œ λ™μž‘ μ˜ˆμ‹œ**이며, μ‚¬μš© μ˜λ¬΄λŠ” μ—†μŠ΅λ‹ˆλ‹€.\n",
    "\n",
    "- μž…λ ₯: `AIF_INPUT_DIR` μ•„λž˜ `pairs.csv`(`id` μ—΄)와 `images/<id>/pre.png`, `images/<id>/post.png`\n",
    "  (각 256x256 RGB 8bit, 두 μ˜μƒμ€ 같은 κ²©μžμ— μ •λ ¬λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€)\n",
    "- 좜λ ₯: `AIF_PREDICTION_PATH`에 `id,new_building,tree_removal` CSV ν•œ 개. 두 열은 각각 κ·Έ 클래슀의 λ³€ν™”\n",
    "  폴리곀 λͺ©λ‘μ„ 적은 JSON λ¬Έμžμ—΄μž…λ‹ˆλ‹€. 폴리곀은 `[x, y]` 점의 λͺ©λ‘(ꡬ멍 μ—†μŒ)이고 λͺ©λ‘ μ•ˆμ— 폴리곀을 μ—¬λŸ¬ 개\n",
    "  담을 수 μžˆμŠ΅λ‹ˆλ‹€. 예: `[[[10,10],[30,10],[30,30],[10,30]]]`. μ’Œν‘œλŠ” ν›„ μ˜μƒ 패치의 ν”½μ…€ μ’Œν‘œ(원점 μ™Όμͺ½ μœ„\n",
    "  λͺ¨μ„œλ¦¬, x 였λ₯Έμͺ½, y μ•„λž˜, μ‹€μˆ˜ ν—ˆμš©)이고 빈 λ¬Έμžμ—΄μ€ κ·Έ 클래슀의 λ³€ν™”κ°€ μ—†μŒμ„ λœ»ν•©λ‹ˆλ‹€. CSV μ…€ μ•ˆμ˜ JSON 은\n",
    "  `csv` λͺ¨λ“ˆμ΄ 인용 처리λ₯Ό ν•΄ μ€λ‹ˆλ‹€\n",
    "- 동봉 κ°€μ€‘μΉ˜: `assets/model/unet_r18_cd.pt` (55MB). μΆ”λ‘  λ‹¨κ³„λŠ” λ„€νŠΈμ›Œν¬κ°€ μ°¨λ‹¨λ˜λ―€λ‘œ ν•„μš”ν•œ\n",
    "  νŒŒμΌμ€ λͺ¨λ‘ zip μ•ˆμ— λ„£μ–΄μ•Ό ν•©λ‹ˆλ‹€\n",
    "- μ˜μ‘΄μ„±: `requirements.txt`둜 μ„ μ–Έν•©λ‹ˆλ‹€(폴리곀 λ³€ν™˜μ— shapely λ₯Ό μ”λ‹ˆλ‹€). 채점 μ„œλ²„κ°€ μ„€μΉ˜ λ‹¨κ³„μ—μ„œ λ¨Όμ € μ„€μΉ˜ν•˜λ―€λ‘œ λ…ΈνŠΈλΆ μ•ˆμ—\n",
    "  μ„€μΉ˜ 셀을 두지 μ•Šμ•˜μŠ΅λ‹ˆλ‹€. `%pip install -r requirements.txt`처럼 μƒλŒ€κ²½λ‘œλ‘œ νŒŒμΌμ„ κ°€λ¦¬ν‚€λŠ”\n",
    "  μ„€μΉ˜ 쀄은 μ„€μΉ˜ λ‹¨κ³„μ˜ μž‘μ—… 폴더가 제좜물 폴더가 μ•„λ‹ˆμ–΄μ„œ μ‹€νŒ¨ν•˜λ―€λ‘œ λ„£μ§€ λ§ˆμ‹œκΈ° λ°”λžλ‹ˆλ‹€\n",
    "- 제좜 κ·œμΉ™: zip μ΅œμƒμœ„μ— `predict.ipynb`(이 파일)와 `requirements.txt`, κ°€μ€‘μΉ˜λŠ” `assets/`에 동봉\n",
    "\n",
    "**둜컬 μ‹€ν–‰**: `pairs.csv`와 `images/<id>/{pre,post}.png`λ₯Ό 담은 폴더λ₯Ό λ§Œλ“€κ³  ν™˜κ²½λ³€μˆ˜λ‘œ λ„˜κΈ°λ©΄ λ©λ‹ˆλ‹€.\n",
    "μ§€μ •ν•˜μ§€ μ•ŠμœΌλ©΄ `./input`을 읽고 `./prediction.csv`에 μ”λ‹ˆλ‹€.\n",
    "```\n",
    "AIF_INPUT_DIR=<μž…λ ₯ 폴더> AIF_PREDICTION_PATH=./prediction.csv jupyter nbconvert --to notebook --execute predict.ipynb\n",
    "```\n",
    "\n",
    "**λΌμ΄μ„ μŠ€**: segmentation_models_pytorch MIT, ResNet18 ImageNet 인코더(torchvision 원본) BSD-3,\n",
    "νŒŒμΈνŠœλ‹ κ°€μ€‘μΉ˜μ™€ 이 μ½”λ“œλŠ” λŒ€νšŒ μ°Έκ°€ λͺ©μ μœΌλ‘œ 자유둭게 μ‚¬μš©, μˆ˜μ •, μž¬ν•™μŠ΅ν•˜μ‹€ 수 μžˆμŠ΅λ‹ˆλ‹€.\n",
    "νŒŒμƒ μ œμΆœλ¬Όμ— μΉ΄ν”Όλ ˆν”„νŠΈλ‚˜ 별도 약관이 μ „νŒŒλ˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. μΆœμ²˜λŠ” `NOTICE`, 원문은 `LICENSE`에\n",
    "μžˆμŠ΅λ‹ˆλ‹€. 이 λ² μ΄μŠ€λΌμΈμ€ μ°Έκ³  κ΅¬ν˜„μ΄λ©° μ‚¬μš© μ˜λ¬΄κ°€ μ—†μŠ΅λ‹ˆλ‹€."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d5f3a0ff",
   "metadata": {},
   "outputs": [],
   "source": "# [setup]\nimport csv\nimport io\nimport json\nimport os\nimport sys\nimport time\nfrom pathlib import Path\n\nimport numpy as np\nimport torch\nfrom PIL import Image\nfrom shapely.geometry import MultiPolygon, Polygon, box\nfrom shapely.ops import unary_union\n\nINPUT_DIR = Path(os.environ.get(\"AIF_INPUT_DIR\", \"./input\"))\nPREDICTION_PATH = Path(os.environ.get(\"AIF_PREDICTION_PATH\", \"./prediction.csv\"))\nCKPT = next(p for p in map(Path, [\"assets/model/unet_synth.pt\", \"assets/model/siam_v2.pt\", \"assets/model/hybrid_v3.pt\"]) if p.exists())      # μ»€λ„μ˜ μž‘μ—… 폴더가 제좜물 λ£¨νŠΈμ΄λ―€λ‘œ μƒλŒ€κ²½λ‘œλ‘œ μ§€μ •ν•©λ‹ˆλ‹€\n\nH, W = 256, 256\nBATCH = 16\nCLASSES = (\"new_building\", \"tree_removal\")       # λͺ¨λΈ 좜λ ₯ 채널 1, 2 (0 은 λ°°κ²½)\nMIN_AREA = {'new_building': 30.0, 'tree_removal': 30.0}            # 이 면적(px^2) 미만의 쑰각은 μ§€μ›λ‹ˆλ‹€ (ν•™μŠ΅ λ•Œμ™€ 같은 ν›„μ²˜λ¦¬)\nMIN_POS_AREA = 20.0      # 쑰각을 μ§€μš΄ λ’€ 남은 총면적이 이 κ°’ 미만이면 빈 예츑으둜 λƒ…λ‹ˆλ‹€ (채점 κ·œμΉ™κ³Ό 동일)\nSIMPLIFY_PX = 0.5        # κ³„λ‹¨ν˜• 외곽선을 이 ν—ˆμš© 였차둜 λ‹¨μˆœν™”ν•©λ‹ˆλ‹€ (μœ„μƒ 보쑴). 채점 ν—ˆμš© 였차 1px μ•ˆμž…λ‹ˆλ‹€\nNDIGITS = 2              # μ’Œν‘œ μ†Œμˆ˜ 자릿수\nMEAN = np.array([0.485, 0.456, 0.406], np.float32)\nSTD = np.array([0.229, 0.224, 0.225], np.float32)\n\n\ndef mask_to_polygons(mask: np.ndarray, min_area: float) -> str:\n    \"\"\"(H,W) 이진 마슀크 -> 폴리곀 λͺ©λ‘ JSON λ¬Έμžμ—΄(폴리곀 = 점 λͺ©λ‘, ꡬ멍 μ—†μŒ). λ³€ν™”κ°€ μ—†μœΌλ©΄ 빈 λ¬Έμžμ—΄.\n\n    ν–‰ λ‹¨μœ„ 런(run)을 ν™”μ†Œ 경계 μ‚¬κ°ν˜•μœΌλ‘œ λ§Œλ“€μ–΄ ν•©μ§‘ν•©ν•˜λ©΄ 마슀크의 외곽선을 κ·ΈλŒ€λ‘œ λ”°λ₯΄λŠ” 폴리곀이 λ‚˜μ˜΅λ‹ˆλ‹€.\n    μ’Œν‘œλŠ” ν™”μ†Œ λͺ¨μ„œλ¦¬ κΈ°μ€€(ν™”μ†Œ (r, c) λŠ” [c, c+1] x [r, r+1])이라 채점 κ·œμ•½κ³Ό κ°™μŠ΅λ‹ˆλ‹€.\n    \"\"\"\n    m = np.asarray(mask, dtype=bool)\n    boxes = []\n    for r in np.flatnonzero(m.any(axis=1)):\n        pad = np.concatenate(([0], m[r].astype(np.int8), [0]))\n        edges = np.flatnonzero(np.diff(pad))\n        for s, e in zip(edges[::2], edges[1::2]):\n            boxes.append(box(float(s), float(r), float(e), float(r + 1)))\n    if not boxes:\n        return \"\"\n    g = unary_union(boxes)\n    parts = list(g.geoms) if isinstance(g, MultiPolygon) else [g]\n    parts = [p for p in parts if isinstance(p, Polygon) and p.area >= min_area]     # μž‘μ€ 쑰각 제거\n    if not parts or sum(p.area for p in parts) < MIN_POS_AREA:\n        return \"\"\n    out = []\n    for p in parts:\n        p = p.simplify(SIMPLIFY_PX, preserve_topology=True)\n        if p.is_empty or p.area <= 0:\n            continue\n        # λ°”κΉ₯ κ²½κ³„λ§Œ μ”λ‹ˆλ‹€(제좜 ν˜•μ‹μ— ꡬ멍이 μ—†μŠ΅λ‹ˆλ‹€). λ§ˆμ§€λ§‰ 점은 첫 점과 κ°™μœΌλ―€λ‘œ λΊλ‹ˆλ‹€\n        out.append([[round(float(x), NDIGITS), round(float(y), NDIGITS)] for x, y in list(p.exterior.coords)[:-1]])\n    return json.dumps(out, separators=(\",\", \":\")) if out else \"\"\n\nTHRESH = {'new_building': 0.5, 'tree_removal': 0.5}   # pixel probability thresholds\nPRES_THRESH = 0.0                                           # presence-head gate (siam only, 0 = off)\n"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "938bc30a",
   "metadata": {},
   "outputs": [],
   "source": [
    "# [inputs]\n",
    "# λͺ©λ‘ νŒŒμΌμ„ μž¬κ·€ νƒμƒ‰μœΌλ‘œ μ°Ύκ³  κ·Έ λΆ€λͺ¨ 폴더λ₯Ό μž…λ ₯ 루트둜 μ‚ΌμŠ΅λ‹ˆλ‹€ (껍데기 폴더 ν•œ 겹에 λŒ€λΉ„).\n",
    "# μΆ”λ‘  λŒ€μƒμ€ λͺ©λ‘ 파일이 정본이며 images/ 폴더λ₯Ό ν›‘μ–΄ λŒ€μƒμ„ μ •ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.\n",
    "print(\"AIF_INPUT_DIR:\", INPUT_DIR, sorted(p.name for p in INPUT_DIR.iterdir()) if INPUT_DIR.is_dir() else \"(μ—†μŒ)\")\n",
    "lists = sorted(INPUT_DIR.rglob(\"pairs.csv\"))\n",
    "if not lists:\n",
    "    # μž…λ ₯이 ν’€λ¦° 폴더가 μ•„λ‹ˆλΌ μ••μΆ•λ³Έ κ·ΈλŒ€λ‘œ μ£Όμ–΄μ§€λŠ” κ²½μš°μ— λŒ€λΉ„ν•©λ‹ˆλ‹€\n",
    "    zs = sorted(INPUT_DIR.rglob(\"*.zip\")) if INPUT_DIR.is_dir() else []\n",
    "    if len(zs) == 1:\n",
    "        import tempfile, zipfile\n",
    "        dst = Path(tempfile.mkdtemp()) / \"input\"\n",
    "        with zipfile.ZipFile(zs[0]) as z:\n",
    "            z.extractall(dst)\n",
    "        print(f\"μž…λ ₯이 μ••μΆ•λ³Έμ΄μ–΄μ„œ ν’€μ–΄μ„œ μ½μŠ΅λ‹ˆλ‹€: {zs[0].name} -> {dst}\")\n",
    "        lists = sorted(dst.rglob(\"pairs.csv\"))\n",
    "if not lists:\n",
    "    raise SystemExit(f\"pairs.csv λ₯Ό μ°Ύμ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€: {INPUT_DIR}\")\n",
    "ROOT = lists[0].parent\n",
    "with lists[0].open(encoding=\"utf-8-sig\", newline=\"\") as f:\n",
    "    IDS = [row[\"id\"].strip() for row in csv.DictReader(f) if row.get(\"id\", \"\").strip()]\n",
    "print(f\"μž…λ ₯ 루트 {ROOT}, 쌍 {len(IDS)}건\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9ab4f29d",
   "metadata": {},
   "outputs": [],
   "source": [
    "# [env]\n",
    "print(\"python\", sys.version.split()[0], \"torch\", torch.__version__)\n",
    "print(\"cuda\", torch.cuda.is_available(), \"mps\", getattr(torch.backends, \"mps\", None) is not None and torch.backends.mps.is_available())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "400e6923",
   "metadata": {},
   "outputs": [],
   "source": [
    "# [model]\n",
    "sys.path.insert(0, \"assets\")\n",
    "import segmentation_models_pytorch as smp\n",
    "from model_v2 import SiamCD\n",
    "ENS_W = 0.5   # weight of the baseline-arch UNet; 1-ENS_W for the Siamese model\n",
    "\n",
    "\n",
    "def load_model(device: str):\n",
    "    u = smp.Unet(encoder_name=\"resnet18\", encoder_weights=None, in_channels=6, classes=3)\n",
    "    u.load_state_dict(torch.load(\"assets/model/unet_synth.pt\", map_location=\"cpu\", weights_only=True)[\"state_dict\"])\n",
    "    import glob\n",
    "    sm = []\n",
    "    for f in sorted(glob.glob(\"assets/model/siam_*.pt\")):          # several Satlas Siamese models -> output average\n",
    "        s = SiamCD()\n",
    "        s.load_state_dict(torch.load(f, map_location=\"cpu\", weights_only=True)[\"state_dict\"])\n",
    "        sm.append(s.to(device).eval()); print(\"loaded\", f, flush=True)\n",
    "    return (u.to(device).eval(), sm)\n",
    "\n",
    "\n",
    "def read_image(path: Path) -> np.ndarray:\n",
    "    with Image.open(io.BytesIO(path.read_bytes())) as im:\n",
    "        arr = np.asarray(im.convert(\"RGB\"))\n",
    "    if arr.shape[:2] != (H, W):\n",
    "        raise ValueError(f\"{path}: 크기 {arr.shape[:2]} κ°€ {(H, W)} 와 λ‹€λ¦…λ‹ˆλ‹€\")\n",
    "    return arr\n",
    "\n",
    "\n",
    "@torch.no_grad()\n",
    "def predict_batch(model, pres, posts, device):\n",
    "    u, s = model\n",
    "    a = torch.from_numpy(np.stack(pres)).permute(0, 3, 1, 2).to(device).float() / 255\n",
    "    b = torch.from_numpy(np.stack(posts)).permute(0, 3, 1, 2).to(device).float() / 255\n",
    "    m = torch.tensor(MEAN, device=device).view(1, 3, 1, 1); sd = torch.tensor(STD, device=device).view(1, 3, 1, 1)\n",
    "    prob, pp = 0, 0\n",
    "    for dims in ([], [3], [2], [2, 3]):\n",
    "        ai, bi = (torch.flip(a, dims), torch.flip(b, dims)) if dims else (a, b)\n",
    "        pu = torch.softmax(u(torch.cat([(ai - m) / sd, (bi - m) / sd], 1)), 1)[:, 1:3]\n",
    "        cs, ps = 0, 0\n",
    "        for sk in s:\n",
    "            with torch.autocast(\"cuda\", dtype=torch.float16, enabled=(device == \"cuda\")):\n",
    "                ch, pr, _, _ = sk(ai, bi)\n",
    "            cs = cs + ch.float().sigmoid() / len(s); ps = ps + pr.float().sigmoid() / len(s)\n",
    "        ci = ENS_W * pu.float() + (1 - ENS_W) * cs\n",
    "        prob = prob + (torch.flip(ci, dims) if dims else ci) / 4\n",
    "        pp = pp + ps / 4\n",
    "    return prob.cpu().numpy(), pp.cpu().numpy()\n",
    "\n",
    "\n",
    "DEVICE = \"cuda\" if torch.cuda.is_available() else (\"mps\" if torch.backends.mps.is_available() else \"cpu\")\n",
    "try:\n",
    "    MODEL = load_model(DEVICE)\n",
    "    predict_batch(MODEL, [np.zeros((H, W, 3), np.uint8)], [np.zeros((H, W, 3), np.uint8)], DEVICE)\n",
    "except Exception as e:  # noqa: BLE001\n",
    "    print(f\"{DEVICE} μ‹œν—˜ 좔둠에 μ‹€νŒ¨ν•΄({type(e).__name__}: {e}) CPU 둜 μ „ν™˜ν•©λ‹ˆλ‹€\")\n",
    "    DEVICE = \"cpu\"\n",
    "    MODEL = load_model(DEVICE)\n",
    "print(\"device\", DEVICE)\n",
    "\n",
    "\n",
    "# ---- CHMv2 canopy height (DINOv3 ViT-L/16 SAT-493M + DPT head, DINOv3 License: assets/chmv2/LICENSE_DINOv3.md)\n",
    "CHM_A = 0.2       # weight of the canopy-height cue in the tree_removal probability\n",
    "try:\n",
    "    import transformers\n",
    "    print(\"python\", sys.version.split()[0], \"transformers\", transformers.__version__, flush=True)\n",
    "    from chm_fuse import CHM, cue_abs, cue_diff\n",
    "    CHM_CUE = cue_diff\n",
    "    CHM_MODEL = CHM(\"assets/chmv2\", DEVICE, scale=1.5)\n",
    "    CHM_MODEL.heights([np.zeros((H, W, 3), np.uint8)])\n",
    "    print(\"CHM loaded\", CHM_A, \"diff\", 1.5, flush=True)\n",
    "except Exception as e:  # noqa: BLE001\n",
    "    print(f\"CHM load failed ({type(e).__name__}: {e}); CHM cue off\", flush=True)\n",
    "    CHM_A = 0.0\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a918ddd1",
   "metadata": {},
   "outputs": [],
   "source": [
    "# [infer]\n",
    "ROWS = []\n",
    "STATS = {c: {\"maxp\": [], \"pres\": []} for c in CLASSES}\n",
    "GRID_T = (0.3, 0.4, 0.5, 0.6, 0.7, 0.8)\n",
    "GRID_A = (20, 50, 100, 200)\n",
    "GRID = {c: np.zeros((len(GRID_T), len(GRID_A)), int) for c in CLASSES}\n",
    "t0 = time.time()\n",
    "for s in range(0, len(IDS), BATCH):\n",
    "    ids = IDS[s:s + BATCH]\n",
    "    pres = [read_image(ROOT / \"images\" / i / \"pre.png\") for i in ids]\n",
    "    posts = [read_image(ROOT / \"images\" / i / \"post.png\") for i in ids]\n",
    "    prob, pp = predict_batch(MODEL, pres, posts, DEVICE)\n",
    "    if CHM_A > 0:\n",
    "        cue = CHM_CUE(CHM_MODEL.heights(pres), CHM_MODEL.heights(posts)).cpu().numpy()\n",
    "        prob[:, 1] = (1 - CHM_A) * prob[:, 1] + CHM_A * cue\n",
    "        STATS.setdefault(\"cue_max\", []).extend(cue.reshape(len(ids), -1).max(1).tolist())\n",
    "    nd = np.stack([(a.max(2) == 0) | (b.max(2) == 0) for a, b in zip(pres, posts)])   # no-data is never scored\n",
    "    prob[:, :, :, :][np.repeat(nd[:, None], 2, 1)] = 0\n",
    "    for j, i in enumerate(ids):\n",
    "        cells = []\n",
    "        for k, c in enumerate(CLASSES):\n",
    "            p = prob[j, k]\n",
    "            STATS[c][\"maxp\"].append(float(p.max()))\n",
    "            if pp is not None:\n",
    "                STATS[c][\"pres\"].append(float(pp[j, k]))\n",
    "            for ti, t in enumerate(GRID_T):\n",
    "                area = int((p > t).sum())\n",
    "                for ai, amin in enumerate(GRID_A):\n",
    "                    GRID[c][ti, ai] += area >= amin\n",
    "            gate = pp is None or PRES_THRESH <= 0 or pp[j, k] >= PRES_THRESH\n",
    "            cells.append(mask_to_polygons(p > THRESH[c], MIN_AREA[c]) if gate else \"\")\n",
    "        ROWS.append((i, *cells))\n",
    "    if (s // BATCH) % 10 == 0 or s + BATCH >= len(IDS):\n",
    "        print(f\"  {min(s + BATCH, len(IDS))}/{len(IDS)}  {time.time() - t0:.1f}s\", flush=True)\n",
    "n_b = sum(1 for _, b, _ in ROWS if b)\n",
    "n_t = sum(1 for _, _, t in ROWS if t)\n",
    "print(f\"좔둠을 μ™„λ£Œν–ˆμŠ΅λ‹ˆλ‹€: {len(ROWS)}건, 증좕 μ–‘μ„± {n_b}건, 벌λͺ© μ–‘μ„± {n_t}건, {time.time() - t0:.1f}s\")\n",
    "print(\"μ„€μ •:\", \"THRESH\", THRESH, \"MIN_AREA\", MIN_AREA, \"PRES_THRESH\", PRES_THRESH)\n",
    "for c in CLASSES:\n",
    "    mp = np.array(STATS[c][\"maxp\"])\n",
    "    print(f\"[{c}] μŒλ³„ μ΅œλŒ€ν™•λ₯  λΆ„μœ„μˆ˜(10/25/50/75/90%):\", np.round(np.percentile(mp, [10, 25, 50, 75, 90]), 3).tolist())\n",
    "    if STATS[c][\"pres\"]:\n",
    "        pr = np.array(STATS[c][\"pres\"])\n",
    "        print(f\"[{c}] μ‘΄μž¬ν™•λ₯  λΆ„μœ„μˆ˜(10/25/50/75/90%):\", np.round(np.percentile(pr, [10, 25, 50, 75, 90]), 3).tolist(),\n",
    "              \" >=0.3/0.5/0.7:\", [int((pr >= v).sum()) for v in (0.3, 0.5, 0.7)])\n",
    "    print(f\"[{c}] μ–‘μ„± 쌍 수 ν‘œ (ν–‰=ν”½μ…€μž„κ³„ {GRID_T}, μ—΄=μ΅œμ†Œλ©΄μ  {GRID_A}):\")\n",
    "    for ti, t in enumerate(GRID_T):\n",
    "        print(\"   \", t, GRID[c][ti].tolist())\n",
    "\n",
    "if STATS.get(\"cue_max\"):\n",
    "    print(\"CHM λ‹¨μ„œ μŒλ³„ μ΅œλŒ€κ°’ λΆ„μœ„μˆ˜(10/50/90%):\", np.round(np.percentile(STATS[\"cue_max\"], [10, 50, 90]), 3).tolist())\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4bd390bc",
   "metadata": {},
   "outputs": [],
   "source": [
    "# [save]\n",
    "PREDICTION_PATH.parent.mkdir(parents=True, exist_ok=True)\n",
    "with PREDICTION_PATH.open(\"w\", newline=\"\", encoding=\"utf-8\") as f:\n",
    "    w = csv.writer(f)\n",
    "    w.writerow([\"id\", *CLASSES])\n",
    "    w.writerows(ROWS)\n",
    "print(\"μ €μž₯ν–ˆμŠ΅λ‹ˆλ‹€:\", PREDICTION_PATH, PREDICTION_PATH.stat().st_size, \"bytes\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "208f89da",
   "metadata": {},
   "source": [
    "## 제좜\n",
    "\n",
    "μ•„λž˜ μ…€μ˜ `<μ°Έμ—¬ν‚€>` μžλ¦¬μ— 본인의 μ°Έμ—¬ν‚€λ₯Ό λ„£κ³  μ‹€ν–‰ν•˜λ©΄ 이 λ…ΈνŠΈλΆμ΄ λ“€μ–΄ μžˆλŠ” 폴더 전체가\n",
    "μ œμΆœλ©λ‹ˆλ‹€. μ°Έμ—¬ν‚€λ₯Ό λ…ΈνŠΈλΆμ— 적은 μ±„λ‘œ κ³΅μœ ν•˜μ§€ λ§ˆμ‹œκΈ° λ°”λžλ‹ˆλ‹€. μ°Έμ—¬ν‚€λŠ” λ§ˆμ΄νŽ˜μ΄μ§€μ—μ„œ\n",
    "μ•„λž˜ 경둜λ₯Ό 따라 μ–Έμ œλ“  λ‹€μ‹œ ν™•μΈν•˜μ‹€ 수 μžˆμŠ΅λ‹ˆλ‹€.\n",
    "\n",
    "ν”„λ‘œν•„ β†’ λ§ˆμ΄νŽ˜μ΄μ§€ β†’ ν™œλ™νžˆμŠ€ν† λ¦¬ νƒ­ β†’ μ°Έκ°€ν•˜μ‹  주제의 ν‚€ 볡사"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}