{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceType":"competition","sourceId":5916,"databundleVersionId":45048}],"dockerImageVersionId":31328,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# 데이터 로드","metadata":{}},{"cell_type":"markdown","source":"## 파일 종류 및 크기 확인 ","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19"}},{"cell_type":"code","source":"import os\n\nINPUT_DIR = \"/kaggle/input/competitions/dstl-satellite-imagery-feature-detection\"\n\nfor f in sorted(os.listdir(INPUT_DIR)):\n    size_mb = os.path.getsize(os.path.join(INPUT_DIR, f)) / 1024 / 1024\n    print(f\"{f:50s} {size_mb:.1f} MB\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-07T05:55:55.638398Z","iopub.execute_input":"2026-05-07T05:55:55.638863Z","iopub.status.idle":"2026-05-07T05:55:55.659808Z","shell.execute_reply.started":"2026-05-07T05:55:55.638840Z","shell.execute_reply":"2026-05-07T05:55:55.658259Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 압축 해제 ","metadata":{}},{"cell_type":"code","source":"import pandas as pd\n\ntrain_wkt = pd.read_csv(f\"{INPUT_DIR}/train_wkt_v4.csv.zip\")\nprint(\"shape: \", train_wkt.shape)\nprint(\"head: \\n\", train_wkt.head(n=20))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-07T05:55:55.662336Z","iopub.execute_input":"2026-05-07T05:55:55.662730Z","iopub.status.idle":"2026-05-07T05:55:56.550672Z","shell.execute_reply.started":"2026-05-07T05:55:55.662679Z","shell.execute_reply":"2026-05-07T05:55:56.549712Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"MultipolygonWKT: 사진 속 객체의 지리적 좌표 (((이면 안에 여러개가 있다는 뜻 \n\nImageId가 6040_2_2인 이미지 안에는 ClassType이 4, 5인 객체들이 여러개 존재한다.","metadata":{}},{"cell_type":"markdown","source":"| ClassType | 클래스명             | 설명                              |\n| --------- | ---------------- | ------------------------------- |\n| 1         | Buildings        | 대형 건물, 주거/비주거, 연료 저장 시설, 요새화 건물 |\n| 2         | Misc. Structures | 기타 인공 구조물                       |\n| 3         | Road             | 도로                              |\n| 4         | Track            | 비포장 도로, 오솔길, 산책로                |\n| 5         | Trees            | 삼림, 산울타리, 나무 군락, 독립 수목          |\n| 6         | Crops            | 경작지, 밀밭, 감자·순무 등 작물             |\n| 7         | Waterway         | 수로 (강, 하천)                      |\n| 8         | Standing Water   | 정체 수역 (호수, 연못)                  |\n| 9         | Vehicle Large    | 대형 차량 (트럭, 버스, 물류 차량)           |\n| 10        | Vehicle Small    | 소형 차량 (승용차, 밴, 오토바이)            |","metadata":{}},{"cell_type":"code","source":"grid_sizes = pd.read_csv(f\"{INPUT_DIR}/grid_sizes.csv.zip\")\nprint(\"shape: \", grid_sizes.shape)\nprint(\"head: \\n\", grid_sizes.head(n=20))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-07T05:55:56.551694Z","iopub.execute_input":"2026-05-07T05:55:56.552162Z","iopub.status.idle":"2026-05-07T05:55:56.564298Z","shell.execute_reply.started":"2026-05-07T05:55:56.552137Z","shell.execute_reply":"2026-05-07T05:55:56.563612Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"P: 모델의 입력 이미지에 Mask를 표시해야 하는데 WKT는 지리적 좌표이지 이미지 안에서의 좌표가 아니다. 어떻게 이미지 안의 좌표로 변환할 수 있을까?\n\nS: 특정 부분(Xmax, Ymin)을 기준점으로 하여 좌표를 변환하자.\n\n- 픽셀 X 좌표: p_x = (x/Xmax)*이미지 가로 크기\n- 픽셀 Y 좌표: p_y = (y/Ymin)*이미지 세로 크기 ","metadata":{}},{"cell_type":"markdown","source":"three_band.zip의 용량이 너무 커서 메모리에 올라가지가 않는다. 압축된 파일 전체를 풀지않고 특정 이미지 몇 장만 추출하여 데이터를 확인해보자. ","metadata":{}},{"cell_type":"code","source":"import zipfile\n\nOUTPUT_DIR = \"/kaggle/working/sample_images\"\nos.makedirs(OUTPUT_DIR, exist_ok=True)\n\nzip_path = f\"{INPUT_DIR}/three_band.zip\"\n\nwith zipfile.ZipFile(zip_path, 'r') as zf:\n    all_tifs = [f for f in zf.namelist() if f.endswith('.tif')]\n    print(f\"전체 이미지 수: {len(all_tifs)}\")\n    print(f\"all_tifs.head(): {all_tifs[:5]}\")\n\n    sample_files = all_tifs[:5]\n    for fname in all_tifs[:5]:\n        zf.extract(fname,  OUTPUT_DIR)\n        ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-07T05:55:56.565846Z","iopub.execute_input":"2026-05-07T05:55:56.566084Z","iopub.status.idle":"2026-05-07T05:55:58.553970Z","shell.execute_reply.started":"2026-05-07T05:55:56.566061Z","shell.execute_reply":"2026-05-07T05:55:58.553300Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"`rasterio`의 `window`를 사용하면 지정된 픽셀 구간만 메모리에 올릴 수 있다.","metadata":{}},{"cell_type":"code","source":"import rasterio\nfrom rasterio.windows import Window\nimport numpy as np\nimport matplotlib.pyplot as plt\n\ntif_path = f\"{OUTPUT_DIR}/three_band/6010_0_4.tif\"\n\nwith rasterio.open(tif_path) as src:\n    print(f\"전체 크기: {src.width} x {src.height} px\")\n    print(f\"밴드 수: {src.count}\")\n    print(f\"데이터 타입: {src.dtypes}\")\n\n    window = Window(0, 0, 512, 512)\n    patch = src.read(window=window)\n\nprint(patch.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-07T05:55:58.554809Z","iopub.execute_input":"2026-05-07T05:55:58.555024Z","iopub.status.idle":"2026-05-07T05:55:59.938439Z","shell.execute_reply.started":"2026-05-07T05:55:58.555002Z","shell.execute_reply":"2026-05-07T05:55:59.937040Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"위성 이미지의 픽셀값을 0~255 사이로 정규화 해준다. 이때 2 ~ 98% percentile 기반 클리핑을 해준다.\n햇빛에 반사된 부분이나 구름에 넓게 가려진 부분은 이상치가 되기 때문이다. 이런 기법은 의료 이미지에서도 자주 쓰인다.\n","metadata":{}},{"cell_type":"code","source":"def normalize_band(band):\n    \"\"\"2~98 퍼센타일 기반 클리핑 후 0-1 정규화\"\"\"\n    p2, p98 = np.percentile(band, (2, 98))\n    return np.clip((band - p2) / (p98 - p2 + 1e-6), 0, 1)\n\n# RGB 조합 (3채널)\nrgb = np.stack([\n    normalize_band(patch[0]),  # R\n    normalize_band(patch[1]),  # G\n    normalize_band(patch[2])   # B\n], axis=-1)  # shape: (512, 512, 3)\n\nplt.figure(figsize=(8, 8))\nplt.imshow(rgb)\nplt.title(\"Three-band sample (512x512 window)\")\nplt.axis('off')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-07T05:55:59.939694Z","iopub.execute_input":"2026-05-07T05:55:59.940262Z","iopub.status.idle":"2026-05-07T05:56:00.408430Z","shell.execute_reply.started":"2026-05-07T05:55:59.940233Z","shell.execute_reply":"2026-05-07T05:56:00.407227Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"이미지가 이상하다. 밴드 순서가 RGB가 이닌가?","metadata":{}},{"cell_type":"code","source":"with rasterio.open(tif_path) as src:\n    print(src.colorinterp)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-07T05:56:00.409728Z","iopub.execute_input":"2026-05-07T05:56:00.410007Z","iopub.status.idle":"2026-05-07T05:56:00.418286Z","shell.execute_reply.started":"2026-05-07T05:56:00.409978Z","shell.execute_reply":"2026-05-07T05:56:00.417165Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"RGB 순서는 맞음, 정규화할 때 범위가 문제인가? ","metadata":{}},{"cell_type":"code","source":"with rasterio.open(tif_path) as src:\n    patch = src.read(window=Window(0, 0, 512, 512))\n\nfor i, band_name in enumerate(['R', 'G', 'B']):\n    b = patch[i]\n    print(f\"Band {band_name}: min={b.min()}, max={b.max()}, \"\n          f\"mean={b.mean():.1f}, dtype={b.dtype}\")\n    print(f\"p2={np.percentile(b,2):.0f}, p98={np.percentile(b,98):.0f}, p98-p2={np.percentile(b,98)-np.percentile(b,2):.0f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-07T05:56:00.419595Z","iopub.execute_input":"2026-05-07T05:56:00.419942Z","iopub.status.idle":"2026-05-07T05:56:00.463359Z","shell.execute_reply.started":"2026-05-07T05:56:00.419913Z","shell.execute_reply":"2026-05-07T05:56:00.461727Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"파랑색의 범위가 빨강과 초록보다 작다. 이 상태에서 정규화를 해버리면 파랑색의 영향이 실제보다 더 커지게 된다. 이미지 전체의 통계값을 사용하여 다시 시각화 해보자","metadata":{}},{"cell_type":"code","source":"with rasterio.open(tif_path) as src:\n    overview = src.read(out_shape=(src.count, src.height//10, src.width//10)).astype(np.float32)\n    stats = [(np.percentile(overview[i], 2), np.percentile(overview[i], 98)) for i in range(3)]\n\nrgb = np.stack([\n    np.clip((patch[i].astype(np.float32) - stats[i][0])\n            / (stats[i][1] - stats[i][0] + 1e-6), 0, 1)\n    for i in range(3)\n], axis=-1)\n\nplt.figure(figsize=(8, 8))\nplt.imshow(rgb)\nplt.title(\"Three-band sample (512x512 window)\")\nplt.axis('off')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-07T05:56:00.464787Z","iopub.execute_input":"2026-05-07T05:56:00.465051Z","iopub.status.idle":"2026-05-07T05:56:00.819941Z","shell.execute_reply.started":"2026-05-07T05:56:00.465023Z","shell.execute_reply":"2026-05-07T05:56:00.818257Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"이미지가 원래 이런건가? 다른 이미지는 어떻게 생겼지?","metadata":{}},{"cell_type":"code","source":"IMAGE_ID = \"6120_2_2\"\ntarget_filename = f\"three_band/{IMAGE_ID}.tif\"\n\nwith zipfile.ZipFile(f\"{INPUT_DIR}/three_band.zip\", \"r\") as zf:\n    if target_filename in zf.namelist():\n        zf.extract(target_filename, OUTPUT_DIR)\n        print(\"success\")\n    else:\n        print(\"extract fail\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-07T05:56:00.822373Z","iopub.execute_input":"2026-05-07T05:56:00.822662Z","iopub.status.idle":"2026-05-07T05:56:01.348917Z","shell.execute_reply.started":"2026-05-07T05:56:00.822638Z","shell.execute_reply":"2026-05-07T05:56:01.347132Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"tif_path = f\"{OUTPUT_DIR}/three_band/{IMAGE_ID}.tif\"\n\nwith rasterio.open(tif_path) as src:\n    print(f\"전체 크기: {src.width} x {src.height} px\")\n    print(f\"밴드 수: {src.count}\")\n    print(f\"데이터 타입: {src.dtypes}\")\n\n    window = Window(1024, 1024, 1024+512, 1024+512)\n    patch = src.read(window=window)\n\nprint(patch.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-07T05:56:01.350730Z","iopub.execute_input":"2026-05-07T05:56:01.351117Z","iopub.status.idle":"2026-05-07T05:56:01.370352Z","shell.execute_reply.started":"2026-05-07T05:56:01.351071Z","shell.execute_reply":"2026-05-07T05:56:01.368596Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# RGB 조합 (3채널)\nrgb = np.stack([\n    normalize_band(patch[0]),  # R\n    normalize_band(patch[1]),  # G\n    normalize_band(patch[2])   # B\n], axis=-1)  # shape: (512, 512, 3)\n\nplt.figure(figsize=(8, 8))\nplt.imshow(rgb)\nplt.title(\"Three-band sample (512x512 window)\")\nplt.axis('off')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-07T05:56:01.371687Z","iopub.execute_input":"2026-05-07T05:56:01.372074Z","iopub.status.idle":"2026-05-07T05:56:01.935682Z","shell.execute_reply.started":"2026-05-07T05:56:01.372034Z","shell.execute_reply":"2026-05-07T05:56:01.934370Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"정규화에는 문제가 없었고 원래 저렇게 생긴 이미지였나 봄 ","metadata":{}},{"cell_type":"markdown","source":"## 정리","metadata":{}},{"cell_type":"code","source":"# 매게 변수 \nINPUT_DIR = \"/kaggle/input/competitions/dstl-satellite-imagery-feature-detection\"\nOUTPUT_DIR = \"/kaggle/working/sample_images\"\nIMAGE_ID = \"6120_2_2\"\nTIF_PATH = f\"{OUTPUT_DIR}/three_band/{IMAGE_ID}.tif\"\n\ntrain_wkt = pd.read_csv(f\"{INPUT_DIR}/train_wkt_v4.csv.zip\")\nwkt_img = train_wkt[train_wkt[\"ImageId\"] == IMAGE_ID]\n\ngrid_sizes = pd.read_csv(f\"{INPUT_DIR}/grid_sizes.csv.zip\", names=[\"ImageId\", \"Xmax\", \"Ymin\"], skiprows=1)\ngrid_img = grid_sizes[grid_sizes[\"ImageId\"] == IMAGE_ID].iloc[0]\nx_max, y_min = grid_img[\"Xmax\"], grid_img[\"Ymin\"]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-07T05:56:01.936778Z","iopub.execute_input":"2026-05-07T05:56:01.937043Z","iopub.status.idle":"2026-05-07T05:56:02.457876Z","shell.execute_reply.started":"2026-05-07T05:56:01.937021Z","shell.execute_reply":"2026-05-07T05:56:02.457247Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"with rasterio.open(TIF_PATH) as src:\n    W, H = src.width, src.height\n    SCHALE = 4\n    overview_stat = src.read(out_shape=(src.count, H//10, W//10)).astype(np.float32)\n    stats = [(np.percentile(overview_stat[i], 2), np.percentile(overview_stat[i], 98)) for i in range(3)]\n    img = src.read(out_shape=(src.count, H//SCHALE, W//SCHALE)).astype(np.float32)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-07T05:56:02.458637Z","iopub.execute_input":"2026-05-07T05:56:02.458816Z","iopub.status.idle":"2026-05-07T05:56:02.525637Z","shell.execute_reply.started":"2026-05-07T05:56:02.458798Z","shell.execute_reply":"2026-05-07T05:56:02.524449Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"dH, dW = img.shape[1], img.shape[2]\nrgb = np.stack([np.clip((img[i]-stats[i][0]) / (stats[i][1]-stats[i][0]+1e-6), 0, 1) for i in range(3)], axis=-1)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-07T05:56:02.526713Z","iopub.execute_input":"2026-05-07T05:56:02.526997Z","iopub.status.idle":"2026-05-07T05:56:02.538999Z","shell.execute_reply.started":"2026-05-07T05:56:02.526967Z","shell.execute_reply":"2026-05-07T05:56:02.538130Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 좌표 변환 함수\n변환 공식:\n\npx = (x_wkt / x_max) * img_w\n\npy = (y_wkt / y_min) * img_h","metadata":{}},{"cell_type":"code","source":"import shapely.wkt\n\ndef wkt_to_pixel_polygons(wkt_str, x_max, y_min, img_w, img_h):\n    #라벨이 없는 이미지 처리\n    if \"EMPTY\" in wkt_str:\n        return []\n\n    # WKT 문자열을 기하적 객체 생성을 도와주는 Shapely geometry 객체로 파싱 \n    geom = shapely.wkt.loads(wkt_str)\n    parts = list(geom.geoms) if geom.geom_type == \"MultiPolygon\" else [geom]\n\n    polygons = []\n    for poly in parts:\n        coords = np.array(poly.exterior.coords)  # exterior: 폴리곤 외곽선 좌표 \n        # shape: (N, 2) - N:(x, y) 좌표 쌍 \n        px = (coords[:, 0] / x_max) * img_w\n        py = (coords[:, 1] / y_min) * img_h\n\n        polygons.append(np.column_stack([px, py]))\n\n    return polygons","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-07T05:56:02.540541Z","iopub.execute_input":"2026-05-07T05:56:02.540939Z","iopub.status.idle":"2026-05-07T05:56:02.551096Z","shell.execute_reply.started":"2026-05-07T05:56:02.540914Z","shell.execute_reply":"2026-05-07T05:56:02.549822Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"CLASS_INFO = {\n    1:  (\"Buildings\",       \"#e74c3c\"),\n    2:  (\"Misc. Structures\",\"#e67e22\"),\n    3:  (\"Road\",            \"#f1c40f\"),\n    4:  (\"Track\",           \"#2ecc71\"),\n    5:  (\"Trees\",           \"#27ae60\"),\n    6:  (\"Crops\",           \"#1abc9c\"),\n    7:  (\"Waterway\",        \"#3498db\"),\n    8:  (\"Standing Water\",  \"#2980b9\"),\n    9:  (\"Vehicle Large\",   \"#9b59b6\"),\n    10: (\"Vehicle Small\",   \"#8e44ad\"),\n}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-07T05:56:02.552632Z","iopub.execute_input":"2026-05-07T05:56:02.553157Z","iopub.status.idle":"2026-05-07T05:56:02.575566Z","shell.execute_reply.started":"2026-05-07T05:56:02.553109Z","shell.execute_reply":"2026-05-07T05:56:02.574396Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.patches as mpatches\nfrom matplotlib.patches import Polygon as MplPolygon\n\nfig, ax = plt.subplots(figsize=(12, 12))\n\n# 이미지 표시\n# imshow는 기본적으로 y=0을 위쪽으로 표시 (이미지 좌표계와 일치)\nax.imshow(rgb)\n\n# axes의 x, y 범위를 이미지 픽셀 크기에 맞게 명시\n# set_ylim(dH, 0): y축 최대값이 위, 최솟값이 아래\n#   → matplotlib 기본(y가 위로 증가)을 뒤집어\n#     이미지 좌표계(y가 아래로 증가)와 폴리곤 좌표를 일치시킴\nax.set_xlim(0, dW)\nax.set_ylim(dH, 0)\nax.set_title(f\"ImageId: {IMAGE_ID} — WKT Polygon Overlay\", fontsize=14)\nax.axis(\"off\")\n\nlegend_handles = []\n\nfor _, row in wkt_img.iterrows():\n    cls  = int(row[\"ClassType\"])\n    name, color = CLASS_INFO.get(cls, (f\"Class {cls}\", \"#ffffff\"))\n\n    # WKT 문자열 → 픽셀 좌표 폴리곤 리스트 변환\n    # dW, dH를 넘겨 다운샘플 이미지 크기 기준으로 변환\n    polys = wkt_to_pixel_polygons(\n        row[\"MultipolygonWKT\"], x_max, y_min, dW, dH\n    )\n\n    if not polys:\n        continue  # EMPTY 클래스는 건너뜀\n\n    for pts in polys:\n        # pts: ndarray(N, 2) — 하나의 폴리곤 픽셀 좌표\n        patch = MplPolygon(\n            pts,\n            closed=True,       # 마지막 점을 첫 점과 연결\n            facecolor=color,   # 내부 채우기 색\n            edgecolor=color,   # 경계선 색\n            alpha=0.4,         # 반투명: 위성 이미지가 비쳐 보임\n            linewidth=1.0\n        )\n        ax.add_patch(patch)  # axes에 폴리곤 패치 추가\n\n    # 범례: 같은 클래스가 여러 폴리곤이어도 한 번만 추가\n    if not any(h.get_label() == f\"{cls}: {name}\" for h in legend_handles):\n        legend_handles.append(\n            mpatches.Patch(facecolor=color, edgecolor=color,\n                           alpha=0.8, label=f\"{cls}: {name}\")\n        )\n\nax.legend(handles=legend_handles, loc=\"upper right\",\n          fontsize=9, framealpha=0.85,\n          title=\"Class Types\", title_fontsize=10)\n\nplt.tight_layout()\nplt.savefig(f\"{OUTPUT_DIR}/{IMAGE_ID}_overlay.png\", dpi=150, bbox_inches=\"tight\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-07T05:56:24.695963Z","iopub.execute_input":"2026-05-07T05:56:24.697089Z","iopub.status.idle":"2026-05-07T05:56:34.746710Z","shell.execute_reply.started":"2026-05-07T05:56:24.697047Z","shell.execute_reply":"2026-05-07T05:56:34.745671Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}