스테이션바/측점(체이니지) - 역 재진입 마커 누락 수정: 측점기준 탐지에 좌표근접(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>
23 KiB
드론 자세 평활/보간 + 영상 오버레이 렌더링 파이프라인
후임자 인계 + 특허 문서용 상세 기술 문서 대상 소스: 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)이다.
이 문서가 다루는 본질적 과제는 두 가지 노이즈/지연 문제이다.
- 자세/GPS 노이즈 — 원시 SRT 로그는 떨림이 있어 그대로 투영하면 라벨이 매 프레임 진동한다. 그러나 단순 평균은 회전 구간을 뭉개 라벨이 늦게 따라온다(지연). → 에지 보존 적응형 평활(smoothFrame) 로 해결.
- 렌더 부드러움 vs 계산 비용 — 라벨 가시성/겹침 판정은 비싸고(전 프레임 × 전 항목), 60fps 매끄러운 이동은 매 프레임 재투영을 요구한다. → 가시성은 사전계산(precompute), 좌표는 RAF에서 연속포즈로 재투영하는 2단 분리로 해결.
2. 핵심 알고리즘
2.1 에지(회전경계) 보존 적응형 이동평균 — smoothFrame
원시 드론 프레임 배열에서 인덱스 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ₖ) )
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
영상의 부드러운 재생시간(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 + (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 방식)
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 청크 처리, 월드좌표 저장
데이터 로드 완료 시(또는 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 겹침 억제 / 우선순위 / 진행방향 변형
화면상 가까이 겹치는 라벨을 솎아낸다. 임계: 가로 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 렌더 루프 (계산 없이 조회 + 투영만)
매 프레임 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. 보간포즈를 받아 매 프레임 직접 투영한다(캐시 미사용 → 라인이 라벨처럼 부드럽게 이동).
- 근거리 클리핑: 세그먼트 양 끝의
Zc가CLIP_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. 특허 관점 신규성
-
에지(회전경계) 보존 적응형 이동평균 — yaw 차 임계로 평균창을 회전경계에서 멈추고, 회전 종료 후 한 프레임씩 점진 재확장하여 직선구간 노이즈 억제와 회전구간 무지연 추종을 동시에 달성. 회전 프레임 재혼입에 의한 1회 튐을 구조적으로 제거(변화율 기반 방식 대비).
-
월드좌표 사전계산 + 연속포즈 재투영의 2단 분리 — 라벨 가시성/겹침/우선순위(비싼 결정)는 월드좌표로 1회 사전계산하고, 화면 위치(가벼운 투영)는 매 RAF 연속 보간포즈로 재계산. "무엇을 그릴지"와 "어디에 그릴지"의 분리로 60fps AR 오버레이를 단일스레드에서 달성.
-
연속 프레임번호 보간 포즈 + 최단각 yaw — 단조 벽시계 보간 재생시간을 실수 프레임번호로 변환, 이산 SRT 자세를 평활-후-보간하여 이산격자(30fps)를 넘는 60fps 연속 자세를 생성. yaw는 원형평균(평활)·최단각(보간)으로 360° 경계 안정.
-
속도적응 화면 평활(One-Euro 사상) + 연속거부 한계 — 속도벡터 평활로 떨림(왕복상쇄)과 실이동(방향일관)을 분리해 alpha를 적응시키고, 이상치 거부에 연속거부 한계를 두어 시크/재등장 시 영구고착을 방지.
-
진행방향 인지 형제 라벨 해소 — 상/하행 형제 구조물이 화면상 겹칠 때 노선 진행방향((상)/(하)) 변형을 우선 표시. 철도역 최우선 + 근접거리 차선의 다단 우선순위와 결합.
-
object-fit:cover 정합 + 양방향 좌표 변환 — 영상 크롭 사각형에 오버레이를 일치시키는 정규좌표 변환과 그 역변환(클릭→월드 역투영)으로, 영상 위 라벨 클릭/드래그 보정(focal 역산·표고 역산·지면교차)을 동일 좌표계에서 처리.
소요 시간: 18분 Context 사용량: input 88k / output 12k tokens