Files
GhiVideo/docs/history/2026-06-29_1436_드론자세평활-오버레이렌더링-기술문서화.md
b23042andClaude Opus 4.8 e9052143d7 feat: 스테이션바 측점 정확도 개선 + 영상 컨트롤 UI 통합 + 종합 기술문서
스테이션바/측점(체이니지)
- 역 재진입 마커 누락 수정: 측점기준 탐지에 좌표근접(pxPassesTo) 합집합 보강(중복 24px 제거)
- kmExists 판정: 좌표 반경만으로 잡힌 통과는 측점값 라벨/검색에서 제외(마커 동그라미는 유지)
- 측점값 라벨 겹침 숨김(좌측 우선), 동일항목 브래킷 클러스터
- 측점 검색 순환(통과방향 다음→끝에서 처음), 마커 기준 후보 산출, 입력값 유지/blur 삭제
- 연속 체이니지 수직투영 유틸 분리(client/src/utils/chainage.ts), Minimap 컴포넌트 추가

영상 컨트롤 UI
- 선형/드론궤적/좌측패널 토글을 재생바로 통합(settingsStore 공유·persist)
- 드론 GPS·고도 HUD 상시 표시, 경로표고·투명도 항상 노출, 겹침제외 버튼 숨김
- 재생바 요소 재배치(좌측패널→선형→드론궤적→드론위치정보→시설등급), 활성색 amber 통일

문서/도구
- 종합 기술명세서(인계+특허 기초) md/html/pdf 3종, UI개선 보고서 3종
- md→html→pdf 변환 스크립트(scripts/md2docs.sh) + 스타일(docs/assets/report.css)
- 작업 히스토리 다수 추가(docs/history/)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 14:56:27 +09:00

23 KiB
Raw Permalink Blame History

드론 자세 평활/보간 + 영상 오버레이 렌더링 파이프라인

후임자 인계 + 특허 문서용 상세 기술 문서 대상 소스: client/src/components/overlay/StationOverlay.tsx (1639줄), 투영 유틸 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, #L126)이다.

이 문서가 다루는 본질적 과제는 두 가지 노이즈/지연 문제이다.

  1. 자세/GPS 노이즈 — 원시 SRT 로그는 떨림이 있어 그대로 투영하면 라벨이 매 프레임 진동한다. 그러나 단순 평균은 회전 구간을 뭉개 라벨이 늦게 따라온다(지연). → 에지 보존 적응형 평활(smoothFrame) 로 해결.
  2. 렌더 부드러움 vs 계산 비용 — 라벨 가시성/겹침 판정은 비싸고(전 프레임 × 전 항목), 60fps 매끄러운 이동은 매 프레임 재투영을 요구한다. → 가시성은 사전계산(precompute), 좌표는 RAF에서 연속포즈로 재투영하는 2단 분리로 해결.

2. 핵심 알고리즘

2.1 에지(회전경계) 보존 적응형 이동평균 — smoothFrame

StationOverlay.tsx#L577

원시 드론 프레임 배열에서 인덱스 i를 중심으로 좌우 최대 halfWin(기본 60프레임 ≈ 2초)까지 이동평균을 취하되, 중심 프레임의 yaw와 차이가 임계 YAW_EDGE = 8° 이내인 인접 프레임까지만 창을 확장한다.

// 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:

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 주석).

2.2 연속 프레임번호 보간 포즈 — poseAt

StationOverlay.tsx#L670

영상의 부드러운 재생시간(60fps 벽시계 보간, §4.1)에서 산출된 실수(實數) 프레임번호 estFrame 에 대응하는 자세를 만든다. 이산 SRT 프레임만으로는 30fps 격자에 갇혀 라벨이 계단식으로 움직이므로, 양 이웃 프레임의 평활 포즈(smoothFrame 결과) 를 선형보간한다.

// 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 + (yx)·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

poseAt로 재투영한 화면 정규좌표를 매 프레임 그대로 쓰면 잔여 떨림이 보인다. 화면단에서 One-Euro 필터 사상(idea)을 적용: 느릴 때(떨림)는 강하게 평활, 빠를 때(실제 팬)는 즉시 추종.

// 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).

추가로 픽셀 히스테리시스가 EMA 위에 한 겹 더 적용된다: 화면 px 위치가 0.75px 이상 움직일 때만 정수 좌표를 갱신해 저속에서 반올림 깜빡임을 제거한다(StationOverlay.tsx#L1048, #L1084).


3. 라벨 사전계산 + 겹침 억제

3.1 startLabelPrecompute — requestIdleCallback 청크 처리, 월드좌표 저장

StationOverlay.tsx#L692

데이터 로드 완료 시(또는 params/평활/범위 변경 시 500ms debounce, #L824) 전 프레임에 대해 어떤 라벨이 보이는가(가시집합)와 겹침 해소 결과를 미리 계산해 Map<frameNum, LabelCache>에 적재한다. 핵심 설계는 화면좌표가 아니라 월드좌표(lat/lon/z)를 저장한다는 점이다(LabelCache 정의 #L123). 가시성/겹침은 한 번만 결정하고, 실제 화면 위치는 RAF가 매 프레임 연속포즈로 재투영한다.

청크 처리. 메인스레드 블로킹을 피하려 requestIdleCallback으로 한 번에 CHUNK = 200프레임씩 처리한다.

// 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): 빈 구간 없이 새 청크가 덮어쓰며 갱신.
  • 취소 토큰 precomputeIdRef: 새 계산이 시작되면 id가 증가해 진행 중이던 step이 자동 중단(중복 작업 방지).

가시판정은 카메라 클리핑(Zc < CLIP_Z = 0.1 제외 = 카메라 뒤/너무 가까움), 화면 밖(pxRaw/pyRaw가 [0.02, 1.02] 밖) 제외, POI는 수평직선거리 distH > MAX_RANGE 제외로 이뤄진다(#L731, #L748).

3.2 겹침 억제 / 우선순위 / 진행방향 변형

StationOverlay.tsx#L755

화면상 가까이 겹치는 라벨을 솎아낸다. 임계: 가로 POI_MERGE_X = 0.10, 세로 POI_MERGE_Y = 0.035(정규화). 비등방(가로 관대, 세로 엄격)은 텍스트 라벨이 가로로 길기 때문.

우선순위 정렬(겹침 시 누가 남는가): ① 철도역/역사(railRank=0)를 최우선, ② 같은 등급 내에서는 드론에 가까운 것(dist 작은 것)을 우선.

// 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)의 변형을 남긴다. baseStruct()로 괄호 변형을 떼 형제임을 판정하고, 이미 수용된 라벨이 비-방향태그인데 새 후보가 방향태그면 교체한다(#L777):

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)(렌더 시 ROW_H = 24px로 중앙정렬, #L1101).


4. RAF 렌더 루프 (계산 없이 조회 + 투영만)

StationOverlay.tsx#L906

매 프레임 requestAnimationFrame에서 캔버스를 다시 그린다. 무거운 계산(가시성/겹침)은 하지 않고, precompute가 채운 가시집합을 조회 + 연속포즈로 재투영 + 그리기만 한다.

4.1 연속 시간 → 보간 포즈

// 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). 일시정지·시크·배속을 모두 보정하므로 라벨/선/궤적이 영상과 동기된 채 60fps로 매끄럽게 흐른다.

4.2 object-fit:cover 정규좌표 → 화면 변환 (coverRef)

영상은 컨테이너를 비율유지·크롭하며 채우므로(CSS object-fit:cover), 오버레이도 같은 cover 사각형에 매핑해야 정합된다.

// 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)와 자동팝업 동기화도 공유한다.

4.3 중심선 / 드론궤적 투영 (buildLines)

StationOverlay.tsx#L606. 보간포즈를 받아 매 프레임 직접 투영한다(캐시 미사용 → 라인이 라벨처럼 부드럽게 이동).

  • 근거리 클리핑: 세그먼트 양 끝의 ZcCLIP_Z=0.1 경계를 가로지르면 교점 t = (CLIP_Z z1)/(z2 z1)로 잘라 투영(#L622) → 카메라 뒤 점이 화면 반대편으로 튀는 것 방지.
  • 화면밖 컬링: 비트마스크 oc() outcode로 양 끝이 같은 변 밖이면 세그먼트 폐기(#L612).
  • 드론궤적: GPS 경로(lat/lon)를 표고 Z에 투영, PW=15 폭 이동평균으로 평활, STEP=3 샘플, MAX_FWD=200m 앞까지만(#L646).

라벨 자체도 캐시의 월드좌표를 dronePose로 재투영 → smoothStep → 히스테리시스 후 그린다(#L1039 측점, #L1071 POI).

4.4 나침반 미니맵 (heading-up)

StationOverlay.tsx#L954. 본 포즈는 ±60fr 평활이라 회전 시 나침반이 지연된다 → 별도로 ±3fr 가벼운 평활 yaw를 보간해 회전에 즉시 반응. --rot CSS 변수로 DOM 회전을 갱신하며, 360° 누적 언랩으로 359°→0° 점프를 방지한다:

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. 가시 구조물(visStructRef, RAF가 매 프레임 채움)마다 라벨 옆에 선명한 DOM 팝업(시설종별/구조형식/연장 등 컴팩트 3필드)을 자동 생성/제거한다. 60fps RAF에서 setState를 남발하면 리렌더 폭주하므로, 가시집합 변화 감지를 150ms 인터벌로 분리해 추가/제거가 있을 때만 setInfoPopups를 호출한다. 팝업의 실제 화면 위치는 다시 RAF가 라벨 표시좌표를 따라 매 프레임 갱신(EMA PA=0.18, 화면하단 근접 시 위로 플립, #L1146).


5. 성능 설계 — 60fps 유지 전략

작업 비용 배치 빈도
가시성/겹침 판정 (전 프레임 × 전 항목) 높음 precompute 1회(+설정변경 시 debounce 재계산), idle 청크 200fr
좌표 투영 (보간포즈 → 화면) 중간 RAF 매 프레임
평활/EMA/히스테리시스 낮음 RAF 매 프레임
팝업 가시집합 setState 리렌더 유발 setInterval 150ms 변화 시에만

핵심 = precompute(가시성)와 RAF(좌표) 분리. "어떤 라벨을 그릴지"(비싼 결정)는 영상 변화와 무관하게 한 번 계산해 두고, "어디에 그릴지"(가벼운 투영)만 매 프레임 한다. 이로써 RAF 루프는 Map 조회 + 행렬 투영 + 캔버스 드로잉만 남아 60fps를 유지한다(파일 상단 주석 #L1).

부가 전략: 사전계산의 idle 청크 처리(메인스레드 비블로킹) + 취소토큰, 현재 프레임 우선 순환계산, 첫 청크 즉시노출, 픽셀 히스테리시스(불필요한 재드로 억제), 팝업 위치 EMA(물결/잔상 방지), 데이터셋별 보정값 localStorage 영속화(#L490).


6. 한계 / 업그레이드 포인트

  • 단일 캐시 프레임키 의존 잔존: 라벨 가시집합은 Math.floor(estFrame)로 정수 프레임의 캐시를 조회(#L1022). 좌표는 연속이지만 가시집합 자체는 프레임 경계에서 바뀐다 → 빠른 배속에서 라벨 등장/퇴장이 미세하게 튈 수 있음.
  • 중심선 per-frame 투영 비용: buildLines가 전 중심선 점을 매 프레임 투영. 점 수가 많아지면 부담 → 화면 근방 구간만 투영하는 공간색인(앞서 컬링 전 prefilter)으로 개선 여지.
  • POI 표고 부정확: POI는 실측 표고가 없어 최근접 선로표고 또는 '드론고도 − N'으로 가정(#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