# 드론 자세 평활/보간 + 영상 오버레이 렌더링 파이프라인 > 후임자 인계 + 특허 문서용 상세 기술 문서 > 대상 소스: [client/src/components/overlay/StationOverlay.tsx](../../client/src/components/overlay/StationOverlay.tsx) (1639줄), 투영 유틸 [client/src/utils/geoProjection.ts](../../client/src/utils/geoProjection.ts) --- ## 1. 기술 개요 / 목적 드론으로 촬영한 철도 선로 주행영상 위에, 측점(스테이션)·선로구조물(교량/터널/구교)·POI·선로 중심선·드론 GPS 궤적·나침반을 **영상 픽셀에 정확히 정합(AR 오버레이)** 시켜 표출하는 렌더링 파이프라인이다. 입력 데이터는 두 종류로, (a) 프레임별 드론 자세/위치 로그(SRT 추출: `frame, lat, lon, altitude, yaw, pitch, roll`)와 (b) 지리정보(측점·중심선·POI·구조물의 위경도+표고)이다. 핵심 변환은 핀홀 카메라 모델 기반의 월드(EPSG:5186 TM ENU) → 카메라 → 정규화 픽셀 투영([geoProjection.ts#L301](../../client/src/utils/geoProjection.ts#L301), [#L126](../../client/src/utils/geoProjection.ts#L126))이다. 이 문서가 다루는 본질적 과제는 **두 가지 노이즈/지연 문제**이다. 1. **자세/GPS 노이즈** — 원시 SRT 로그는 떨림이 있어 그대로 투영하면 라벨이 매 프레임 진동한다. 그러나 단순 평균은 회전 구간을 뭉개 라벨이 늦게 따라온다(지연). → **에지 보존 적응형 평활(smoothFrame)** 로 해결. 2. **렌더 부드러움 vs 계산 비용** — 라벨 가시성/겹침 판정은 비싸고(전 프레임 × 전 항목), 60fps 매끄러운 이동은 매 프레임 재투영을 요구한다. → **가시성은 사전계산(precompute), 좌표는 RAF에서 연속포즈로 재투영**하는 2단 분리로 해결. --- ## 2. 핵심 알고리즘 ### 2.1 에지(회전경계) 보존 적응형 이동평균 — `smoothFrame` [StationOverlay.tsx#L577](../../client/src/components/overlay/StationOverlay.tsx#L577) 원시 드론 프레임 배열에서 인덱스 `i`를 중심으로 좌우 최대 `halfWin`(기본 60프레임 ≈ 2초)까지 이동평균을 취하되, **중심 프레임의 yaw와 차이가 임계 `YAW_EDGE = 8°` 이내인 인접 프레임까지만 창을 확장**한다. ```typescript // StationOverlay.tsx:577 const smoothFrame = useCallback((frames: DroneFrameBasic[], i: number, halfWin: number): DroneFrameBasic => { let lo = i, hi = i; if (frames.length > 1 && halfWin > 0) { const YAW_EDGE = 8; // deg — 중심 대비 이 이상 벌어지면 다른(회전) 구간으로 보고 평균 제외 const yc = frames[i].yaw; const near = (idx: number) => Math.abs(((frames[idx].yaw - yc + 540) % 360) - 180) <= YAW_EDGE; const loMin = Math.max(0, i - halfWin); const hiMax = Math.min(frames.length - 1, i + halfWin); while (lo > loMin && near(lo - 1)) lo--; // 회전경계에서 멈춤 while (hi < hiMax && near(hi + 1)) hi++; } ``` **수식.** 창 `[lo, hi]`(요소 수 `n = hi − lo + 1`)에 대해 lat/lon/altitude/pitch/roll은 산술평균. yaw는 각도이므로 단순 산술평균 시 0°/360° 경계에서 깨진다 → **원형평균(circular mean)**: ``` yaw̄ = atan2( (1/n)·Σ sin(yawₖ), (1/n)·Σ cos(yawₖ) ) ``` [StationOverlay.tsx#L590](../../client/src/components/overlay/StationOverlay.tsx#L590): ```typescript const yr = f.yaw * Math.PI / 180; sinYaw += Math.sin(yr); cosYaw += Math.cos(yr); // ... yaw: Math.atan2(sinYaw / n, cosYaw / n) * 180 / Math.PI, ``` `near()`의 `((Δ + 540) % 360) − 180`은 yaw 차를 항상 `[−180°, 180°]`로 정규화하는 최단각 차이 패턴이다. **의도 / 트레이드오프.** - 직선 주행: yaw가 거의 일정 → 창이 풀폭(±60fr)으로 펴져 GPS/자세 노이즈를 강하게 억제. - 회전 중: 회전경계에서 창이 좁아져(한쪽 또는 양쪽이 중심 근처에서 멈춤) **실제 자세를 즉시 추종(지연 없음)**. - 회전 종료 후: 회전 프레임을 배제한 채 창이 **한 프레임씩** 점진적으로 재확장 → 튐 없음. - 트레이드오프: `YAW_EDGE`가 너무 작으면 완만한 곡선도 회전으로 보아 노이즈 억제가 약해지고, 너무 크면 회전이 평균에 섞여 지연. 8°는 떨림(보통 <2°)과 실제 회전을 가르는 경험값. - (주석에 기록된) 과거 '변화율 기반 창 축소' 방식은 회전 종료 시 창이 한꺼번에 재확장되며 회전 프레임이 다시 섞여 한 번 튀는 부작용이 있어 경계보존 방식으로 교체됨 ([StationOverlay.tsx#L569](../../client/src/components/overlay/StationOverlay.tsx#L569) 주석). ### 2.2 연속 프레임번호 보간 포즈 — `poseAt` [StationOverlay.tsx#L670](../../client/src/components/overlay/StationOverlay.tsx#L670) 영상의 부드러운 재생시간(60fps 벽시계 보간, §4.1)에서 산출된 **실수(實數) 프레임번호 `estFrame`** 에 대응하는 자세를 만든다. 이산 SRT 프레임만으로는 30fps 격자에 갇혀 라벨이 계단식으로 움직이므로, 양 이웃 프레임의 **평활 포즈(smoothFrame 결과)** 를 선형보간한다. ```typescript // StationOverlay.tsx:670 — 이진탐색으로 estFrame을 끼우는 두 프레임 찾기 let lo = 0, hi = arr.length - 1; while (lo < hi) { const m = (lo + hi) >> 1; if (arr[m].frame < estFrame) lo = m + 1; else hi = m; } const i2 = lo, i1 = Math.max(0, lo - 1); const f1 = arr[i1].frame, f2 = arr[i2].frame; const frac = f2 > f1 ? Math.min(1, Math.max(0, (estFrame - f1) / (f2 - f1))) : 0; const a = smoothFrame(arr, i1, h), b = smoothFrame(arr, i2, h); const L = (x: number, y: number) => x + (y - x) * frac; let dy = b.yaw - a.yaw; dy = ((dy + 540) % 360) - 180; // 최단각 return { ...a, frame: estFrame, lat: L(a.lat, b.lat), lon: L(a.lon, b.lon), altitude: L(a.altitude, b.altitude), pitch: L(a.pitch, b.pitch), roll: L(a.roll, b.roll), yaw: a.yaw + dy * frac }; ``` **수식.** 보간계수 `frac = (estFrame − f1)/(f2 − f1)`. 위치/피치/롤은 `L(x,y) = x + (y−x)·frac`. yaw는 359°→1° 같은 경계에서 긴 쪽으로 도는 것을 막기 위해 **최단각 보간**: `Δ = ((b.yaw − a.yaw + 540) % 360) − 180`, `yaw = a.yaw + Δ·frac`. **트레이드오프.** O(log n) 이진탐색으로 호출당 비용이 낮아 매 RAF 프레임 호출 가능. smoothFrame을 매 프레임 두 번 호출하므로(`a`, `b`) 평활창이 매우 클 때는 비용이 늘 수 있으나, 실데이터 규모에서는 무시 가능. ### 2.3 속도적응 EMA + 이상치 거부 — `smoothStep` (One-Euro 방식) [StationOverlay.tsx#L48](../../client/src/components/overlay/StationOverlay.tsx#L48) `poseAt`로 재투영한 화면 정규좌표를 매 프레임 그대로 쓰면 잔여 떨림이 보인다. 화면단에서 One-Euro 필터 사상(idea)을 적용: **느릴 때(떨림)는 강하게 평활, 빠를 때(실제 팬)는 즉시 추종**. ```typescript // StationOverlay.tsx:48 function smoothStep(prev, tx, ty, maxAlpha, minAlpha, speedRef): DispPos { if (!prev) return { x: tx, y: ty, rej: 0, vx: 0, vy: 0 }; const dx = tx - prev.x, dy = ty - prev.y; const d = Math.hypot(dx, dy); if (d > REJECT_DIST && prev.rej < MAX_REJECT_FRAMES) { return { x: prev.x, y: prev.y, rej: prev.rej + 1, vx: prev.vx, vy: prev.vy }; // 이상치: 위치 유지 } // 속도 벡터 평활 → 떨림(왕복)은 상쇄돼 작게, 실제 이동은 유지. const vx = prev.vx + (dx - prev.vx) * SMOOTH_VEL_BETA; const vy = prev.vy + (dy - prev.vy) * SMOOTH_VEL_BETA; const speed = Math.hypot(vx, vy); const a = Math.min(maxAlpha, minAlpha + (maxAlpha - minAlpha) * Math.min(1, speed / Math.max(1e-4, speedRef))); return { x: prev.x + dx * a, y: prev.y + dy * a, rej: 0, vx, vy }; } ``` **수식.** - 이상치 거부: 프레임당 점프 `d = ‖(dx,dy)‖`가 `REJECT_DIST = 0.12`(정규화 화면거리)를 넘으면 갱신을 버리고 이전 위치 유지. 단 연속 거부가 `MAX_REJECT_FRAMES = 8`을 넘으면(=시크/재등장 같은 진짜 큰 이동) 수용 → 영구 고착 방지. - 속도 평활: `v ← v + (Δ − v)·β`, `β = SMOOTH_VEL_BETA = 0.25`. 떨림은 매 프레임 방향이 번갈아(왕복) `v`가 상쇄돼 ≈0, 실제 이동은 방향이 일관돼 `v` 유지(저크 제거). - 적응 alpha: `α = min(maxAlpha, minAlpha + (maxAlpha − minAlpha)·min(1, speed/speedRef))`. - 정지/떨림(`speed≈0`) → `α≈minAlpha`(기본 0.12, 강한 평활). - 빠른 팬(`speed ≥ speedRef`, 기본 `speedRef=0.010`) → `α=maxAlpha`(기본 `emaAlpha=1.0`, 즉시 추종). - 위치 갱신: `pos ← pos + Δ·α`. **의도 / 트레이드오프.** 입력이 이미 smoothFrame(±60fr) + poseAt 보간으로 한 차례 평활됐으므로 화면 EMA의 `maxAlpha`는 가볍게 둔다(주석: 과거 고정 α=0.01은 지연 ~1.65s로 라벨이 늦게 따라옴). `minAlpha`↓ / `speedRef`↑ 일수록 1배속 떨림은 줄지만 미세 추종 지연이 늘어난다. 이 세 값은 패널에서 데이터셋별로 실시간 튜닝·저장된다([StationOverlay.tsx#L494](../../client/src/components/overlay/StationOverlay.tsx#L494)). 추가로 **픽셀 히스테리시스**가 EMA 위에 한 겹 더 적용된다: 화면 px 위치가 0.75px 이상 움직일 때만 정수 좌표를 갱신해 저속에서 반올림 깜빡임을 제거한다([StationOverlay.tsx#L1048](../../client/src/components/overlay/StationOverlay.tsx#L1048), [#L1084](../../client/src/components/overlay/StationOverlay.tsx#L1084)). --- ## 3. 라벨 사전계산 + 겹침 억제 ### 3.1 `startLabelPrecompute` — requestIdleCallback 청크 처리, 월드좌표 저장 [StationOverlay.tsx#L692](../../client/src/components/overlay/StationOverlay.tsx#L692) 데이터 로드 완료 시(또는 params/평활/범위 변경 시 500ms debounce, [#L824](../../client/src/components/overlay/StationOverlay.tsx#L824)) 전 프레임에 대해 **어떤 라벨이 보이는가(가시집합)와 겹침 해소 결과**를 미리 계산해 `Map`에 적재한다. 핵심 설계는 **화면좌표가 아니라 월드좌표(lat/lon/z)를 저장**한다는 점이다([LabelCache 정의 #L123](../../client/src/components/overlay/StationOverlay.tsx#L123)). 가시성/겹침은 한 번만 결정하고, 실제 화면 위치는 RAF가 매 프레임 연속포즈로 재투영한다. **청크 처리.** 메인스레드 블로킹을 피하려 `requestIdleCallback`으로 한 번에 `CHUNK = 200`프레임씩 처리한다. ```typescript // StationOverlay.tsx:717 const step = () => { if (precomputeIdRef.current !== id) return; // 취소됨 (새 계산이 시작되면 id 불일치 → 중단) const end = Math.min(processed + CHUNK, total); while (processed < end) { const idx = (startIdx + processed) % total; // startIdx→끝→0→… 순환 processed++; const drone = smoothFrame(frames, idx, SMOOTH_HALF); // ... 측점/POI 가시판정 + 겹침억제 → newMap.set(drone.frame, {...}) } if (!published) { labelMapRef.current = newMap; published = true; } // 첫 청크 후 즉시 노출 if (processed < total) requestIdleCallback(step, { timeout: 200 }); }; step(); // 첫 청크는 동기 실행 → 정지 중에도 토글/재계산 즉시 반영 ``` 세 가지 영리한 설계가 있다. - **현재 프레임부터 순환 처리** (`startIdx = currentFrameIdxRef.current`, `idx = (startIdx + processed) % total`): 지금 화면에 보이는 구간을 먼저 계산해 재생 즉시 라벨이 뜬다. - **재계산 시 옛 맵 복사로 시작** ([#L696](../../client/src/components/overlay/StationOverlay.tsx#L696)): 빈 구간 없이 새 청크가 덮어쓰며 갱신. - **취소 토큰** `precomputeIdRef`: 새 계산이 시작되면 `id`가 증가해 진행 중이던 step이 자동 중단(중복 작업 방지). 가시판정은 카메라 클리핑(`Zc < CLIP_Z = 0.1` 제외 = 카메라 뒤/너무 가까움), 화면 밖(`pxRaw/pyRaw`가 [−0.02, 1.02] 밖) 제외, POI는 수평직선거리 `distH > MAX_RANGE` 제외로 이뤄진다([#L731](../../client/src/components/overlay/StationOverlay.tsx#L731), [#L748](../../client/src/components/overlay/StationOverlay.tsx#L748)). ### 3.2 겹침 억제 / 우선순위 / 진행방향 변형 [StationOverlay.tsx#L755](../../client/src/components/overlay/StationOverlay.tsx#L755) 화면상 가까이 겹치는 라벨을 솎아낸다. 임계: 가로 `POI_MERGE_X = 0.10`, 세로 `POI_MERGE_Y = 0.035`(정규화). 비등방(가로 관대, 세로 엄격)은 텍스트 라벨이 가로로 길기 때문. **우선순위 정렬**(겹침 시 누가 남는가): ① 철도역/역사(`railRank=0`)를 최우선, ② 같은 등급 내에서는 드론에 가까운 것(`dist` 작은 것)을 우선. ```typescript // StationOverlay.tsx:757 const railRank = (cat: string): number => (cat === '철도역' || cat === '역사' ? 0 : 1); poiCand.sort((a, b) => railRank(a.category) - railRank(b.category) || a.dist - b.dist); const dirTag = routeDirRef.current === '상' ? '(상)' : routeDirRef.current === '하' ? '(하)' : ''; ``` **진행방향 변형 우선**: 같은 구조물이 상/하행 형제로 두 개 있을 때(예: `회덕제1가도교(상)` / `회덕제1가도교(하)`), 드론이 실제 지나는 선로 방향(routeMeta의 direction, [#L447](../../client/src/components/overlay/StationOverlay.tsx#L447))의 변형을 남긴다. `baseStruct()`로 괄호 변형을 떼 형제임을 판정하고, 이미 수용된 라벨이 비-방향태그인데 새 후보가 방향태그면 교체한다([#L777](../../client/src/components/overlay/StationOverlay.tsx#L777)): ```typescript const k = accepted[overlapIdx]; if (dirTag && baseStruct(c.title) === baseStruct(k.title) && c.title.includes(dirTag) && !k.title.includes(dirTag)) { accepted[overlapIdx] = c; } ``` 겹침제외 토글 OFF면 억제 없이 전부 표시하며, 동일좌표 라벨은 `labelRow / labelRowCount`로 세로 행 배치된다([#L784](../../client/src/components/overlay/StationOverlay.tsx#L784))(렌더 시 `ROW_H = 24px`로 중앙정렬, [#L1101](../../client/src/components/overlay/StationOverlay.tsx#L1101)). --- ## 4. RAF 렌더 루프 (계산 없이 조회 + 투영만) [StationOverlay.tsx#L906](../../client/src/components/overlay/StationOverlay.tsx#L906) 매 프레임 `requestAnimationFrame`에서 캔버스를 다시 그린다. **무거운 계산(가시성/겹침)은 하지 않고**, precompute가 채운 가시집합을 조회 + 연속포즈로 재투영 + 그리기만 한다. ### 4.1 연속 시간 → 보간 포즈 ```typescript // StationOverlay.tsx:941 const estTime = timeRef ? timeRef.current : currentTimeSecRef.current + (performance.now() - timeUpdateWallRef.current) / 1000; const estFrame = estTime * VIDEO_FPS; // VIDEO_FPS = 30000/1001 ≈ 29.97 const dronePose = poseAt(estFrame) ?? (...폴백 smoothFrame...); const lines = buildLines(dronePose); ``` `timeRef`는 VideoPlayer의 `smoothTimeRef`로, 매 RAF마다 단조(monotonic) 벽시계 보간으로 갱신되는 재생시간이다([VideoPlayer.tsx#L84](../../client/src/components/player/VideoPlayer.tsx#L84)). 일시정지·시크·배속을 모두 보정하므로 라벨/선/궤적이 영상과 동기된 채 60fps로 매끄럽게 흐른다. ### 4.2 object-fit:cover 정규좌표 → 화면 변환 (`coverRef`) 영상은 컨테이너를 비율유지·크롭하며 채우므로(CSS `object-fit:cover`), 오버레이도 같은 cover 사각형에 매핑해야 정합된다. ```typescript // StationOverlay.tsx:923 const { w: vW, h: vH } = videoSizeRef.current; let dispW = W, dispH = H, offX = 0, offY = 0; if (vW > 0 && vH > 0) { const s = Math.max(W / vW, H / vH); // cover: 더 큰 배율(넘침=크롭) dispW = vW * s; dispH = vH * s; offX = (W - dispW) / 2; offY = (H - dispH) / 2; // 음수 = 화면 밖(크롭) } coverRef.current = { offX, offY, dispW, dispH, W, H }; const vx = (nx: number) => offX + nx * dispW; // 정규(0~1, 영상프레임) → 화면 px const vy = (ny: number) => offY + ny * dispH; ``` `coverRef`는 RAF가 매 프레임 갱신하고, 포인터 핸들러(클릭→정규좌표 역변환 `clientToVideoNorm`, [#L1211](../../client/src/components/overlay/StationOverlay.tsx#L1211))와 자동팝업 동기화도 공유한다. ### 4.3 중심선 / 드론궤적 투영 (`buildLines`) [StationOverlay.tsx#L606](../../client/src/components/overlay/StationOverlay.tsx#L606). 보간포즈를 받아 매 프레임 직접 투영한다(캐시 미사용 → 라인이 라벨처럼 부드럽게 이동). - **근거리 클리핑**: 세그먼트 양 끝의 `Zc`가 `CLIP_Z=0.1` 경계를 가로지르면 교점 `t = (CLIP_Z − z1)/(z2 − z1)`로 잘라 투영([#L622](../../client/src/components/overlay/StationOverlay.tsx#L622)) → 카메라 뒤 점이 화면 반대편으로 튀는 것 방지. - **화면밖 컬링**: 비트마스크 `oc()` outcode로 양 끝이 같은 변 밖이면 세그먼트 폐기([#L612](../../client/src/components/overlay/StationOverlay.tsx#L612)). - 드론궤적: GPS 경로(lat/lon)를 표고 `Z`에 투영, `PW=15` 폭 이동평균으로 평활, `STEP=3` 샘플, `MAX_FWD=200m` 앞까지만([#L646](../../client/src/components/overlay/StationOverlay.tsx#L646)). 라벨 자체도 캐시의 월드좌표를 `dronePose`로 재투영 → `smoothStep` → 히스테리시스 후 그린다([#L1039](../../client/src/components/overlay/StationOverlay.tsx#L1039) 측점, [#L1071](../../client/src/components/overlay/StationOverlay.tsx#L1071) POI). ### 4.4 나침반 미니맵 (heading-up) [StationOverlay.tsx#L954](../../client/src/components/overlay/StationOverlay.tsx#L954). 본 포즈는 ±60fr 평활이라 회전 시 나침반이 지연된다 → **별도로 ±3fr 가벼운 평활 yaw**를 보간해 회전에 즉시 반응. `--rot` CSS 변수로 DOM 회전을 갱신하며, **360° 누적 언랩**으로 359°→0° 점프를 방지한다: ```typescript const target = -(rawYaw + paramsRef.current.yawOffset); const delta = (((target - minimapRotRef.current) % 360) + 540) % 360 - 180; minimapRotRef.current += delta; minimapRef.current.style.setProperty('--rot', `${minimapRotRef.current}deg`); ``` ### 4.5 자동 컴팩트 팝업 동기화 (setInterval 150ms) [StationOverlay.tsx#L376](../../client/src/components/overlay/StationOverlay.tsx#L376). 가시 구조물(`visStructRef`, RAF가 매 프레임 채움)마다 라벨 옆에 선명한 DOM 팝업(시설종별/구조형식/연장 등 컴팩트 3필드)을 자동 생성/제거한다. **60fps RAF에서 setState를 남발하면 리렌더 폭주**하므로, 가시집합 변화 감지를 **150ms 인터벌**로 분리해 추가/제거가 있을 때만 `setInfoPopups`를 호출한다. 팝업의 실제 화면 위치는 다시 RAF가 라벨 표시좌표를 따라 매 프레임 갱신(EMA `PA=0.18`, 화면하단 근접 시 위로 플립, [#L1146](../../client/src/components/overlay/StationOverlay.tsx#L1146)). --- ## 5. 성능 설계 — 60fps 유지 전략 | 작업 | 비용 | 배치 | 빈도 | |------|------|------|------| | 가시성/겹침 판정 (전 프레임 × 전 항목) | 높음 | precompute | 1회(+설정변경 시 debounce 재계산), idle 청크 200fr | | 좌표 투영 (보간포즈 → 화면) | 중간 | RAF | 매 프레임 | | 평활/EMA/히스테리시스 | 낮음 | RAF | 매 프레임 | | 팝업 가시집합 setState | 리렌더 유발 | setInterval 150ms | 변화 시에만 | **핵심 = precompute(가시성)와 RAF(좌표) 분리.** "어떤 라벨을 그릴지"(비싼 결정)는 영상 변화와 무관하게 한 번 계산해 두고, "어디에 그릴지"(가벼운 투영)만 매 프레임 한다. 이로써 RAF 루프는 Map 조회 + 행렬 투영 + 캔버스 드로잉만 남아 60fps를 유지한다([파일 상단 주석 #L1](../../client/src/components/overlay/StationOverlay.tsx#L1)). 부가 전략: 사전계산의 idle 청크 처리(메인스레드 비블로킹) + 취소토큰, 현재 프레임 우선 순환계산, 첫 청크 즉시노출, 픽셀 히스테리시스(불필요한 재드로 억제), 팝업 위치 EMA(물결/잔상 방지), 데이터셋별 보정값 localStorage 영속화([#L490](../../client/src/components/overlay/StationOverlay.tsx#L490)). --- ## 6. 한계 / 업그레이드 포인트 - **단일 캐시 프레임키 의존 잔존**: 라벨 가시집합은 `Math.floor(estFrame)`로 정수 프레임의 캐시를 조회([#L1022](../../client/src/components/overlay/StationOverlay.tsx#L1022)). 좌표는 연속이지만 가시집합 자체는 프레임 경계에서 바뀐다 → 빠른 배속에서 라벨 등장/퇴장이 미세하게 튈 수 있음. - **중심선 per-frame 투영 비용**: `buildLines`가 전 중심선 점을 매 프레임 투영. 점 수가 많아지면 부담 → 화면 근방 구간만 투영하는 공간색인(앞서 컬링 전 prefilter)으로 개선 여지. - **POI 표고 부정확**: POI는 실측 표고가 없어 최근접 선로표고 또는 '드론고도 − N'으로 가정([#L744](../../client/src/components/overlay/StationOverlay.tsx#L744)) → 드래그/DEM override로 수동보정. DEM 자동조회 통합이 업그레이드 포인트. - **VFR/프레임 누락**: `VIDEO_FPS` 상수(29.97) 가정. 가변프레임율 영상은 frame↔time 매핑 오차 발생. - **평활 파라미터 수동 튜닝**: YAW_EDGE/REJECT_DIST/speedRef 등이 상수 또는 수동슬라이더 → 데이터셋 통계 기반 자동 캘리브레이션 가능. - **단일 스레드**: precompute가 메인스레드 idle 의존. Web Worker로 옮기면 대용량에서 더 안정. --- ## 7. 특허 관점 신규성 1. **에지(회전경계) 보존 적응형 이동평균** — yaw 차 임계로 평균창을 회전경계에서 멈추고, 회전 종료 후 한 프레임씩 점진 재확장하여 *직선구간 노이즈 억제*와 *회전구간 무지연 추종*을 동시에 달성. 회전 프레임 재혼입에 의한 1회 튐을 구조적으로 제거(변화율 기반 방식 대비). 2. **월드좌표 사전계산 + 연속포즈 재투영의 2단 분리** — 라벨 가시성/겹침/우선순위(비싼 결정)는 월드좌표로 1회 사전계산하고, 화면 위치(가벼운 투영)는 매 RAF 연속 보간포즈로 재계산. "무엇을 그릴지"와 "어디에 그릴지"의 분리로 60fps AR 오버레이를 단일스레드에서 달성. 3. **연속 프레임번호 보간 포즈 + 최단각 yaw** — 단조 벽시계 보간 재생시간을 실수 프레임번호로 변환, 이산 SRT 자세를 평활-후-보간하여 이산격자(30fps)를 넘는 60fps 연속 자세를 생성. yaw는 원형평균(평활)·최단각(보간)으로 360° 경계 안정. 4. **속도적응 화면 평활(One-Euro 사상) + 연속거부 한계** — 속도벡터 평활로 떨림(왕복상쇄)과 실이동(방향일관)을 분리해 alpha를 적응시키고, 이상치 거부에 *연속거부 한계*를 두어 시크/재등장 시 영구고착을 방지. 5. **진행방향 인지 형제 라벨 해소** — 상/하행 형제 구조물이 화면상 겹칠 때 노선 진행방향((상)/(하)) 변형을 우선 표시. 철도역 최우선 + 근접거리 차선의 다단 우선순위와 결합. 6. **object-fit:cover 정합 + 양방향 좌표 변환** — 영상 크롭 사각형에 오버레이를 일치시키는 정규좌표 변환과 그 역변환(클릭→월드 역투영)으로, 영상 위 라벨 클릭/드래그 보정(focal 역산·표고 역산·지면교차)을 동일 좌표계에서 처리. --- **소요 시간**: 18분 **Context 사용량**: input 88k / output 12k tokens