쉬운설명_KMZ에서_영상표출까지_전과정

KMZ 파일에서 영상 위 이름표까지 — 전 과정 그림 설명서

이 글은 초등학생도 이해할 수 있게 그림과 이야기로 쓴 설명서예요. "지도 파일(KMZ) 하나가 어떤 여행을 거쳐, 드론 영상 속 정확한 자리에 다리·터널 이름표로 나타날까?" 그 여정을 8단계로 하나씩 따라가 봅니다. (파일·줄 번호는 2026-07-03 기준)


한눈에 보는 전체 여정

KMZ는 "지도 위 핀들을 모아 둔 선물 상자"예요. 이 상자가 열려서, 안의 핀 하나하나가 영상 화면의 정확한 픽셀에 이름표로 붙기까지 — 전체 흐름은 이렇습니다.

① KMZ 파일 (압축 선물상자) ② 압축 풀기 doc.kml 꺼내기 ③ KML 읽기 폴더→핀(Placemark) ④ 목록 완성 POI·구조물 (이름+위경도) 측점 CSV (실측 표고) ⑤ 높이 결정에 사용 드론 비행로그 (SRT/CSV) 위치·자세(yaw·pitch·roll)·초점 ⑥⑦ 좌표변환 + 투영 위경도→미터→카메라→화면 ⑧ 영상 위 이름표 60fps 정합 + 떨림 평활 — KMZ의 핀(④) + 측점 표고(⑤) + 드론 포즈를 합쳐 매 프레임 계산(⑥⑦) → 화면(⑧) —
그림 1. KMZ → 압축해제 → 파싱 → 목록 → (표고·드론 포즈 결합) → 좌표변환·투영 → 영상 표출

1단계 — KMZ는 '압축된 선물 상자'

코드: geoData.ts:295 parseKmz().kml 우선, 없으면 .kmzunzipSync로 해제해 doc.kml을 꺼냄. 실패하면 경고 후 건너뜀.

2단계 — 설명서(KML) 속은 '폴더 나무 + 핀'

KML은 XML이라는 형식의 글이에요. 그 안은 폴더 나무 모양이고, 잎사귀마다 핀(Placemark) 이 달려 있어요.

KMZ 상자 속 doc.kml (폴더 나무) 📁 Document 📁 02)지장물 — 📍📍📍 (POI) 📁 03)교량 — 📍📍 (구조물) 📁 04)터널 — 📍📍 (구조물) 📁 05)출입문번호 — 📍 (POI) 📁 06)구교 — 📍 (구조물) 📍 철도역(KAKAO_RAIL) → POI+역사 폴더 이름을 보고 핀의 '종류'를 결정 핀(Placemark) 하나의 해부도 <name> 대전천교 — 핀의 이름 <description> 속성표(HTML 표) 구분=대전천교 · 연장(m)=124.5 · 시설종별=2종 … → 표의 (키, 값)을 한 줄씩 뽑아 저장 <coordinates> 127.41, 36.33, 12 ← 경도,위도[,고도] 좌표가 없으면 속성표의 lat/lon 사용 · 한국 범위(위도33~39, 경도124~132) 밖이면 버림
그림 2. (왼쪽) 폴더 나무를 따라 내려가며 핀 수집 · (오른쪽) 핀 1개에서 이름·속성표·좌표를 추출

프로그램은 나무를 재귀적으로(가지 끝까지) 걸어 내려가면서, 각 핀이 어느 폴더에 속했는지를 함께 기억해요.

코드: geoData.ts:376 walk() — 폴더 트리 재귀. geoData.ts:276 parseKmlDescProps() — 속성표(HTML <tr><td>) 파싱. 구글어스 재저장본의 태그 변형(<tbody>, &gt; 엔티티 등)도 견고하게 처리.

3단계 — 핀을 종류별로 나누기

폴더 이름이 곧 분류 기준이에요. 학교에서 반 나누듯이:

폴더 분류 어디에 표시되나
03)교량 · 04)터널 · 06)구교 구조물 (bridge/tunnel) 영상 라벨 + 하단 스테이션바 마커
02)지장물 · 05)출입문 POI (지점) 영상 라벨
철도역 (KAKAO_RAIL) POI + 역사 구조물 영상 라벨 + 스테이션바 양쪽

코드: geoData.ts:331 handlePlacemark() — 폴더명 분기. geoData.ts:539 loadGeoData — KMZ 누락 시 kmzMissing 경고.

4단계 — 높이(표고) 정하기: "핀은 몇 m 높이에 있나?"

KMZ의 핀은 평면 위치(위도·경도)는 정확하지만, 높이(z)는 없거나 부정확한 경우가 많아요. 높이가 틀리면 이름표가 하늘에 뜨거나 땅에 파묻혀 보여요. 그래서 이렇게 정합니다.

땅(선로) 바다 기준면(지오이드) — '정표고 0m' POI(교량) 정표고 z = 측점 표고 🛸 드론 고도 = '타원체고' 기준 → 기준이 서로 다름! 높이 기준 맞추기(datum 정합) 타원체고 = 정표고 + 지오이드고(대전 ≈ 25.8m)
그림 3. 핀의 높이는 가장 가까운 측점의 실측 표고를 빌려 쓰고, 드론 고도와 기준을 맞추기 위해 지오이드고(+25.8m)를 더한다

높이 결정 우선순위 (셋 중 위에서부터):

  1. 드래그 보정값 — 사용자가 화면에서 직접 고친 값 (5단계 뒤 '보너스' 참고)
  2. DEM 자동 조회값 — open-meteo API로 받은 그 지점 지면고도 (선택 기능)
  3. 최근접 선로(측점) 표고 — 기본값. 측점 CSV의 실측 정표고를 빌려 씀

이 "측점 표고 빌려 쓰기" 덕분에 비싼 지형데이터(DEM) 없이도 하향각 오차가 42.7°→11.0°로 줄었어요.

코드: StationOverlay.tsx:767 — 지면고도 gz 결정. geoProjection.ts geoidOffset — datum 정합.

5단계 — 좌표 바꾸기: 주소(위경도)를 '미터 자'로

위도·경도는 둥근 지구 위의 주소라서, 그대로는 "몇 m 옆에 있나"를 계산하기 어려워요. 그래서 3번 갈아탑니다.

🌍 위도·경도 (도) 36.33°N, 127.41°E 둥근 지구 주소 🗺 평면 지도 좌표 (m) EPSG:5186 한국 TM 동쪽 x m · 북쪽 y m 🛸 드론 기준 (m) 동·북·위 상대거리 "드론에서 동쪽 40m…" 📷 카메라 눈 (Xc, Yc, Zc) 6단계에서 proj4 빼기(−드론) 회전행렬
그림 4. 위경도 → 한국 TM 평면(m) → 드론 기준 상대좌표 → 카메라 좌표, 3번의 갈아타기

코드: geoProjection.ts:75 EPSG:5186 정의, geoProjection.ts:334 projectPoint() 1부 — ENU 상대좌표 계산.

6단계 — 카메라의 눈으로 회전: "드론이 보는 방향은?"

드론이 어느 쪽을 보고 있는지(비행로그의 yaw·pitch·roll)에 따라, 같은 핀도 화면 왼쪽에 보일 수도, 오른쪽에 보일 수도 있어요. 그래서 상대좌표를 회전행렬로 돌려서 "카메라가 보는 세계"의 좌표 (Xc, Yc, Zc)로 바꿉니다.

R = Rz(−yaw) × Rx(pitch) × Ry(roll)     ← 드론의 고개 돌림·끄덕임·갸웃을 그대로 재현
(Xc, Yc, Zc) = 카메라축변환 × Rᵀ × (동, 북, 위)

코드: geoProjection.ts:301 toCameraCoords(), 회전 수식 주석은 geoProjection.ts:319~.

7단계 — 핀홀 투영: 3D 세상을 2D 사진으로

카메라는 바늘구멍 사진기와 같아요. 3D 점이 구멍(렌즈)을 지나 필름(화면)에 맺힙니다.

카메라(핀홀) 화면(필름) 교량 핀 (Xc, Yc, Zc) 맺힌 자리 = 화면 위치! 가로 위치 = 0.5 + (Xc ÷ Zc) × (초점거리 ÷ 센서폭) 세로 위치 = 0.5 + (Yc ÷ Zc) × (초점거리 ÷ 센서높이)
그림 5. 핀홀 투영 — "멀수록(Zc↑) 가운데로 모이고, 옆에 있을수록(Xc↑) 가장자리로" 라는 원근법 그 자체

코드: geoProjection.ts:126 pixelFromCamera() — 위 공식 그대로.

8단계 — 화면에 붙이기: 액자 맞추기 + 떨림 잡기

마지막으로 "0~1 정규 좌표"를 진짜 화면 픽셀로 바꿔 이름표를 놓아요. 여기엔 두 가지 마무리가 있어요.

① 액자 맞추기 (object-fit: cover) 플레이어 창(파랑 점선) 안에 영상(빨강)이 꽉 차게 늘어나 양옆이 잘림 → 오버레이도 같은 사각형으로 계산해야 어긋나지 않음 정규좌표(0~1) × 영상표시영역 + 잘림offset = 화면 px ② 떨림 잡기 (속도적응 평활) GPS 떨림 그대로 → 라벨이 덜덜덜 평활 후 → 착 붙음 떨림(방향 왔다갔다)=강하게 진정 · 실제 이동(방향 일정)=즉시 따라감
그림 6. (왼쪽) 영상이 창에 '꽉 차게' 잘려 보이므로 오버레이도 같은 규칙으로 정렬 · (오른쪽) 떨림만 골라 진정시키는 평활
  1. 액자 맞추기(cover 정렬) — 영상은 플레이어 창을 비율 유지한 채 꽉 채우며 잘려 보여요. 그래서 이름표도 "창 기준"이 아니라 "잘린 영상 사각형 기준" 으로 놓아야 정확히 붙어요.
  2. 떨림 잡기(평활) — GPS·자세값은 미세하게 떨려요. "방향이 왔다갔다(=떨림)"면 강하게 진정시키고, "방향이 일정(=실제 이동)"하면 즉시 따라가는 속도적응 평활로, 지연 없이 떨림만 없앱니다.
  3. 이 계산 전부를 매 화면 프레임(60fps, requestAnimationFrame)마다 다시 해요 — 그런데도 빠른 이유는, 무거운 준비(가시성 선별)는 미리 해 두고 프레임마다는 가벼운 재투영만 하기 때문이에요.

코드: StationOverlay.tsx:948 cover 정렬, StationOverlay.tsx:49 smoothStep() 평활.


보너스 — 그래도 어긋나면? "드래그 한 번"

지오코딩(주소→좌표 변환) 자체에 오차가 있으면 이름표가 조금 비껴 붙을 수 있어요. 그럴 땐 이름표를 마우스로 끌어 제자리에 놓기만 하면 돼요.

코드: geoProjection.ts:149 worldFromPixel() — 역투영.


단계별 요약표

단계 하는 일 비유 코드 위치
1 KMZ 압축 해제 → doc.kml 선물상자 열기 geoData.ts parseKmz
2 KML 폴더 나무 걷기, 핀 수집 설명서 읽기 geoData.ts walk
3 폴더명으로 POI/구조물 분류 반 나누기 geoData.ts handlePlacemark
4 높이 결정(측점 표고) + 기준 맞춤(+25.8m) 키 재기, 자 통일 StationOverlay + geoidOffset
5 위경도 → 평면 m → 드론 기준 상대좌표 주소를 미터 자로 geoProjection.ts projectPoint
6 yaw·pitch·roll 회전 → 카메라 좌표 카메라 눈으로 보기 geoProjection.ts toCameraCoords
7 핀홀 투영 → 화면 정규좌표(0~1) 바늘구멍 사진기 geoProjection.ts pixelFromCamera
8 cover 정렬 + 평활 + 60fps 렌더 액자 맞추고 떨림 잡기 StationOverlay.tsx
어긋나면 드래그 1회 역투영 보정 이름표 손으로 옮기기 geoProjection.ts worldFromPixel

더 깊은 수식·구현 근거: 「기술명세_GhiVideo_종합기술문서」 · 「구현상세_드론좌표_영상투영_소스코드매칭」 참조.