# GhiVideo 발표 자료 ## 드론 GPS · 영상 · POI 좌표를 영상 위 정확한 위치에 — 그리고 프레임에서 측점(스테이션)으로 > 작성일: 2026-06-30 · 발표용 요약 문서 > 슬라이드 구분은 `---` 입니다 (Marp/reveal.js 변환 가능). 각 장은 "그림 → 핵심 → 코드 근거" 순. --- ## 목차 | 🟦 공간 정합 | 🟩 시간 동기화 | 🟧 표시 안정 | 🟦 측점 전환 | |---|---|---|---| | 지도좌표 → 화면픽셀 | 영상시각 ↔ 드론프레임 | 떨림·튐 제거 | 프레임 → 측점(공간) | 1. 우리가 풀어야 했던 문제 2. 입력 데이터 3종 — GPS · 영상 · POI 3. 전체 파이프라인 한 장 4. **핵심 ①** 좌표 정합 — 지도 좌표를 화면 픽셀로 (투영) 5. **핵심 ②** 시간 동기화 — 영상 시각과 드론 프레임 맞추기 6. **핵심 ③** 라벨 안정화 — 떨림·튐 제거 7. **핵심 ④** 프레임 기반 → 측점(스테이션) 기반 전환 8. 정확도 보정 도구 (DEM · 드래그 · 세로화각) 9. 정리 — 무엇을, 어떻게 --- ## 1. 우리가 풀어야 했던 문제

드론이 철도 노선을 따라 비행하며 찍은 영상 위에, 교량·터널·역사·지장물(POI)의 이름표를 실제 위치에 정확히 띄우고 싶다.

🌉 회덕제1가도교 🚇 법동터널 GPS 36.334, 127.456 · 고도 123m · 측점 157K970 N 드론 영상 + 실제 위치에 붙는 이름표 + 위치 HUD
그림 1. 목표 — 흔들리는 드론 영상 위에, 지도 좌표만 가진 POI를 "진짜 그 자리"에 표시.
**난이도 4가지** → 그래서 **세 가지 정합**이 필요합니다. | 난이도 | 무엇이 어려운가 | 해결 정합 | |---|---|---| | 드론 흔들림 | yaw/pitch/roll·고도 변화·빠른 이동 | 🟦 공간 + 🟧 안정 | | POI는 지도좌표만 | "화면 어디"인지 모름 | 🟦 공간(투영) | | 시간 어긋남 | 영상 시각 ↔ 드론 데이터 시각 | 🟩 시간 | | 매끄러움 | 60fps·떨림 없이 | 🟧 안정 | --- ## 2. 입력 데이터 3종
① 드론 로그 프레임별 비행 CSV frame lat yaw 0 36.33 12° 1 36.33 13° … alt·pitch·roll ② 영상 mp4/webm 주행영상 ③ POI / 측점 KMZ(1순위)+측점CSV 🌉 교량 (lat,lon,z) 🚇 터널 (lat,lon,z) 📍 측점 157K970 이 셋을 시간·공간으로 묶는다
그림 2. 드론 로그(언제·어디·어느 방향) + 영상 + 지도 POI를 결합.
--- ## 3. 전체 파이프라인 한 장
🟩 영상 재생 시각 t smoothTimeRef: 60fps 단조보간 t → 드론 프레임 보간 → 그 순간의 위치·자세(pose) 🟦 좌표 정합 (투영) — 4단계 POI(위경도·표고) → ENU(평면 미터) → 카메라 좌표 (yaw/pitch/roll 회전) → 화면 정규픽셀(0~1) [핀홀 카메라] → object-fit:cover 보정 → 실제 화면 px 🟧 라벨 평활(One Euro) + 이상치 거부 → 떨림 제거 Canvas RAF 60fps 렌더 → 영상 위 이름표 🟦 GPS→측점 투영 → 하단 측점바 커서
그림 3. 시각 t → pose 보간 → 4단계 투영 → 평활 → 렌더. 동시에 GPS는 측점값으로 투영되어 측점바로.
--- ## 4. 핵심 ① 좌표 정합 — 지도 좌표를 화면 픽셀로

"POI는 위경도만 안다. 그게 지금 화면 어디에 보이는가?" — 이게 이 프로그램의 심장. 4단계 투영으로 푼다.

위경도+표고POI 🌉 (지도) 평면 미터ENU · 1단계 카메라 좌표회전 · 2단계 화면 0~1핀홀 · 3단계 화면 pxcover · 4단계
그림 4. 4단계 투영 파이프라인. 파일: client/src/utils/geoProjection.ts
--- ### 4-1. 둥근 지구를 평평하게 (ENU 변환) 지구는 둥글어 거리 계산이 어렵습니다. 노선 한 구간만 잘라 **평평한 모눈종이(미터 단위)** 로 폅니다.
둥근 지구 (위경도) EPSG:5186 TM 투영 E 동(m) N 북(m) 기준점(0,0,0) POI (E=120, N=300, U=-5)
그림 5. E/N = 동·북 몇 m, U = 기준점 대비 상대 높이(alt − refAlt).
```ts // geoToEnu (91–97) const [e, n] = latLonToTM(lat, lon); // 위경도 → 평면 미터 return [e, n, alt - refAlt]; ``` --- ### 4-2. 드론이 보는 방향으로 회전 (카메라 좌표) 세상 좌표(E/N/U)를 드론의 **yaw·pitch·roll** 회전행렬에 곱해 "카메라가 보는 좌표(Xc, Yc, Zc)"로 바꿉니다.
세상 기준 (북 고정) E N 🚁 북쪽 봄 POI 회전 드론이 보는 기준 (정면이 기준) 🚁 앞(Zc) 옆(side) POI (앞 50m, 옆 8m)
그림 6. 북 기준 좌표를 "드론 정면 기준"으로 회전. 진행방향 거리 fwd(앞)·side(옆)도 함께 산출.
```ts // toCameraCoords (301–317) cc.fwd = relEnu[0]*sy + relEnu[1]*cy; // +면 앞쪽 cc.side = relEnu[0]*cy - relEnu[1]*sy; // +면 오른쪽 → "앞은 멀리, 옆은 가깝게" 비등방 필터 근거 ``` --- ### 4-3. 3D를 납작한 사진으로 (핀홀 카메라 투영) 핵심 한 줄: **깊이(Zc)로 나눈다 → 멀수록 화면 가운데로 작게.** (바늘구멍 사진기 원리)
구멍(렌즈) 가까운 🌉 먼 🌉 화면(0~1) 가까운 게 크게 맺힘 거리(Zc)가 클수록 → 화면에 작게
그림 7. pxRaw = 0.5 + (Xc/Zc)·(f/sensorW). 0.5=정중앙, f/sensor=화각.
```ts // pixelFromCamera (126–137) pxRaw = (0.5 + cx0) + (Xc / Zc) * (f / sW); // 가로 0~1 pyRaw = (0.5 + cy0) + (Yc / Zc) * (f / sH); // 세로 0~1 ``` --- ### 4-4. 높이 기준 통일 (지오이드 보정) + 화면 정렬 **두 개의 '0층' 문제**: 지도 높이(정표고·해발)와 GPS 높이(타원체고)는 기준이 달라 대전 기준 약 **25.8m** 차이. 안 맞추면 라벨이 위아래로 어긋납니다.
타원체고(GPS) 0 정표고(해발) 0 ≈ 25.8m (geoidOffset) 지도 높이에 더해 GPS 기준으로 통일 기준을 맞춰야 라벨이 제 높이에 붙는다
그림 8. 지도 표고 + geoidOffset → GPS와 같은 기준. 마지막에 object-fit:cover 잘림을 반영해 0~1 → 실제 px.
```ts geoToEnu(lat, lon, targetAlt + params.geoidOffset, ...); // 높이 기준 통일 // 이후 coverRef(StationOverlay 921–930)로 정규(0~1) → 실제 화면 px 정렬 ``` ➡ 4-1 ~ 4-4를 거치면 **POI가 영상 속 진짜 그 자리에** 찍힙니다. --- ## 5. 핵심 ② 시간 동기화 — 영상 시각 ↔ 드론 프레임 좌표가 맞아도 **"지금 영상 시각의 드론 위치"** 를 못 집으면 라벨이 엉뚱한 데 뜹니다. **문제**: 브라우저 `video.currentTime`은 ~250ms 간격으로만 갱신 → 커서·라벨이 뚝뚝 끊김. **해결**: `smoothTimeRef` — 벽시계로 **60fps 단조 보간**.
실제 currentTime (250ms 띄엄띄엄) → 라벨/커서가 뚝뚝 끊김 smoothTimeRef (매 프레임 채움 · 60fps) media + 경과시간×배속 · 0.3s 이상 벌어지면 재동기화
그림 9. 띄엄띄엄한 실제 시각을 60fps로 메워 부드럽게. 이 t로 드론 pose를 보간.
```ts // VideoPlayer.tsx 62–117 let est = a.media + ((performance.now() - a.wall)/1000) * rate; if (real - est > 0.3) est = real; // 재동기화 smoothTimeRef.current = t; // ref로 노출 → 리렌더 없이 매 프레임 읽음 ``` --- ## 6. 핵심 ③ 라벨 안정화 — 떨림·튐 제거 드론은 미세하게 흔들립니다. 그대로 투영하면 라벨이 부르르 떨립니다. → **One Euro 방식 속도적응 평활 + 이상치 거부.**
떨림(왕복) 평활속도≈0 → 강하게 평활 (안정) 실제 이동 방향 일관 → 즉시 추종 (지연 없음) 갑작스런 점프 8프레임까지 무시 후 수용 (시크는 진짜 → 결국 수용)
그림 10. 떨림은 죽이고, 진짜 이동은 즉시 따라가고, 튐(점프)은 무시. 세 동작을 한 필터로.
```ts // StationOverlay.tsx 48–61 if (d > REJECT_DIST && prev.rej < MAX_REJECT_FRAMES) // 0.12 초과 점프 = 이상치 return { ...prev, rej: prev.rej + 1 }; // 위치 유지 const a = min(maxAlpha, minAlpha + (maxAlpha-minAlpha)*min(1, speed/speedRef)); return { x: prev.x + dx*a, y: prev.y + dy*a, ... }; // 속도 클수록 빠르게 추종 ``` --- ## 7. 핵심 ④ 프레임 기반 → 측점(스테이션) 기반 ### 7-1. 왜 바꿨나? — 시간축의 한계 철도 현장은 **위치(측점)** 로 말합니다: "157K970 지점의 교량". 그런데 시간축은 **드론이 호버(정지)** 하면 무너집니다.
시간축 (균등) 0s1s2s3s4s5s 드론 호버(정지) 시간은 가는데 위치는 안 변함 → 시간축에선 구분 불가 ✗
그림 11. 시간 균등축에선 "공중 대기(호버)" 구간을 표현할 수 없다.
➡ 그래서 **측점(공간)축**으로 재해석: 드론이 호버하면 측점도 멈춰 있어야 자연스럽다. --- ### 7-2. 구현 ① — GPS를 측점값으로 투영 드론 GPS를 측점 폴리라인(선로)에 **수직으로 내려** "측점값(km)"과 "선로 이격(m)"을 구합니다.
157K900 158K000 측점 폴리라인(선로) 🚁 드론 GPS km=157K958, 이격 4.2m projectToChain (수직 내림)
그림 12. 측점명 "157K970" → 157970m 환산으로 선로를 만들고, GPS를 수직 투영해 측점값·이격 산출.
```ts // chainage.ts projectToChain (35–50) const t = clamp(((px-a.x)*dx + (py-a.y)*dy) / L2, 0, 1); // 선분 위 투영 비율 bestKm = a.km + (b.km - a.km) * t; // 보간 측점값 ``` --- ### 7-3. 구현 ② — 이동거리축 (핵심 아이디어) 시간축 대신 **드론이 실제로 이동한 거리**를 축으로 씁니다. 측점값 변화량 |Δ|을 매 프레임 누적.
측점값(평활): 920 → 930 → 940 → 940 940 940 → 950 → 960 누적 이동거리 = 축 위치 정상 주행 →→→ 호버: 누적 멈춤 → 커서 정지! →→→ 주행 호버 구간에서 커서가 멈춰 "공중 대기"가 한눈에 보인다
그림 13. |Δ측점값|을 누적해 0~1로 정규화 → 축 위치. 호버는 누적이 멈추므로 커서도 정지.
```ts // StationBar.tsx 233–274 sm[i] = (pre[hi]-pre[lo])/(hi-lo); // 측점값 ±8프레임 평활 if (i>0) cum += Math.abs(sm[i]-sm[i-1]); // |Δ| 누적 = 실제 이동거리 frac[i] = cum / total; // 0~1 정규화 → 축 위치 ``` --- ### 7-4. 측점 기반의 결과 — 하단 측점바
🚉역 🌉교량 🚇터널 🌉구교 ◌종점 현재 위치 커서 (60fps) 회색=미도착
그림 14. 측점·구조물을 공간축에 배치. 영상별 FPS 자동산출로 배지 정밀화 · 종점 미도착은 회색 + 빈 링.
--- ### 7-5. 프레임 ↔ 측점 — 한 장 비교 | 구분 | 프레임 기반 | 측점 기반 (현재) | |------|-------------|------------------| | 축 | 시간(초) | 실제 이동거리/측점(km) | | 호버 | 구분 불가 | 커서 정지로 가시화 | | 현장 용어 | "3분 20초" | "157K970 교량" | | 위치 질의 | 어려움 | GPS→측점 투영으로 즉답 | | 동기화 | currentTime | smoothTimeRef + 측점투영 | ➡ **시간축 영상을 공간축(측점)으로 재해석**한 것이 이 프로그램의 정체성. --- ## 8. 정확도 보정 도구 투영은 카메라 파라미터·표고 가정에 민감 → **사용자가 미세보정**할 수 있게 함. 핵심은 투영의 **역방향**(화면→세계)을 푸는 것.
영상 화면 라벨을 끌어 제자리로 실제 땅 위경도 역산 화면점 → 광선 → 지면 교차 → 실제 위치 groundPointFromPixel
그림 15. "끌어다 맞추기" = 역투영. 끈 화면점을 광선으로 쏴 지면과 만나는 실제 좌표를 복원.
| 도구 | 동작 | |---|---| | ① **DEM 자동표고** | 모든 POI를 실제 지형고도로 (서버 `/api/elevation`, SRTM 30m) | | ② **드래그 편집** | 라벨을 끌면 `groundPointFromPixel`로 위경도 역산 | | ③ **세로화각 보정** | POI를 실제 위치로 끌면 `sensorH`만 역산 자동보정 | | ④ **보정값 저장** | `poi_overrides.json` 내보내기/가져오기 | --- ## 9. 정리 — 무엇을, 어떻게
드론 GPS 영상 POI 좌표 🟩 시간 동기화smoothTimeRef60fps 보간 🟦 좌표 투영ENU→카메라→핀홀→cover+geoid 보정 🟧 평활One Euro +이상치 거부 영상 위정확한 라벨✨ 🟦 GPS→측점 투영 → 이동거리축→ 측점 기반 측점바 (호버까지 표현)
그림 16. 3대 정합(공간·시간·안정) + 패러다임 전환(프레임→측점).
**3대 정합 기술** 1. **🟦 공간**: 4단계 투영(ENU→카메라회전→핀홀→cover) + 지오이드 보정 + 역투영 보정 2. **🟩 시간**: smoothTimeRef 60fps 단조보간으로 영상시각↔드론프레임 정합 3. **🟧 안정**: One Euro 속도적응 평활 + 이상치 거부 **패러다임 전환** - 프레임(시간) 기반 → **측점(공간) 기반**: GPS를 선로에 투영해 측점값화, 이동거리축으로 호버까지 표현 --- ## 부록 — 핵심 소스 색인 | 기술 | 파일 | 라인 | |------|------|------| | ENU 변환 | geoProjection.ts | 91–97 | | 카메라 좌표 + fwd/side | geoProjection.ts | 301–317 | | 핀홀 투영 | geoProjection.ts | 126–137 | | 지오이드 보정 | geoProjection.ts | 53–67, projectPoint | | 역투영(보정) | geoProjection.ts | 149–251 | | object-fit cover 정렬 | StationOverlay.tsx | 921–930 | | 라벨 평활/이상치 | StationOverlay.tsx | 48–61 | | RAF 60fps 렌더 | StationOverlay.tsx | 905–1198 | | smoothTimeRef | VideoPlayer.tsx | 62–117 | | 측점 투영(chainage) | chainage.ts | 10–64 | | 이동거리축 | StationBar.tsx | 233–274 | | 종점역 미도착 | StationBar.tsx | 300–310, 574–586 | | DEM 표고 | StationOverlay.tsx / elevation.ts | 1384–1415 / 14–67 | > 상세 구현은 [구현상세_GhiVideo_기술-소스코드매칭.md](구현상세_GhiVideo_기술-소스코드매칭.md) 참조.