Files
GhiVideo_v4/docs/기술명세_GhiVideo_종합기술문서.html
T
b23042andClaude Fable 5 d38b842e8d GhiVideo 소스 복제 — v4 작업 시작 기준
기존 GhiVideo 저장소 HEAD의 트래킹 소스 362개 파일을 복제.
(node_modules·storage·빌드 산출물·대용량 미디어는 .gitignore 규칙대로 제외)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 14:32:51 +09:00

835 lines
73 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang xml:lang>
<head>
<meta charset="utf-8" />
<meta name="generator" content="pandoc" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
<title>기술명세_GhiVideo_종합기술문서</title>
<style>
code{white-space: pre-wrap;}
span.smallcaps{font-variant: small-caps;}
span.underline{text-decoration: underline;}
div.column{display: inline-block; vertical-align: top; width: 50%;}
div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;}
ul.task-list{list-style: none;}
pre > code.sourceCode { white-space: pre; position: relative; }
pre > code.sourceCode > span { display: inline-block; line-height: 1.25; }
pre > code.sourceCode > span:empty { height: 1.2em; }
code.sourceCode > span { color: inherit; text-decoration: inherit; }
div.sourceCode { margin: 1em 0; }
pre.sourceCode { margin: 0; }
@media screen {
div.sourceCode { overflow: auto; }
}
@media print {
pre > code.sourceCode { white-space: pre-wrap; }
pre > code.sourceCode > span { text-indent: -5em; padding-left: 5em; }
}
pre.numberSource code
{ counter-reset: source-line 0; }
pre.numberSource code > span
{ position: relative; left: -4em; counter-increment: source-line; }
pre.numberSource code > span > a:first-child::before
{ content: counter(source-line);
position: relative; left: -1em; text-align: right; vertical-align: baseline;
border: none; display: inline-block;
-webkit-touch-callout: none; -webkit-user-select: none;
-khtml-user-select: none; -moz-user-select: none;
-ms-user-select: none; user-select: none;
padding: 0 4px; width: 4em;
color: #aaaaaa;
}
pre.numberSource { margin-left: 3em; border-left: 1px solid #aaaaaa; padding-left: 4px; }
div.sourceCode
{ }
@media screen {
pre > code.sourceCode > span > a:first-child::before { text-decoration: underline; }
}
code span.al { color: #ff0000; font-weight: bold; } /* Alert */
code span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */
code span.at { color: #7d9029; } /* Attribute */
code span.bn { color: #40a070; } /* BaseN */
code span.bu { } /* BuiltIn */
code span.cf { color: #007020; font-weight: bold; } /* ControlFlow */
code span.ch { color: #4070a0; } /* Char */
code span.cn { color: #880000; } /* Constant */
code span.co { color: #60a0b0; font-style: italic; } /* Comment */
code span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */
code span.do { color: #ba2121; font-style: italic; } /* Documentation */
code span.dt { color: #902000; } /* DataType */
code span.dv { color: #40a070; } /* DecVal */
code span.er { color: #ff0000; font-weight: bold; } /* Error */
code span.ex { } /* Extension */
code span.fl { color: #40a070; } /* Float */
code span.fu { color: #06287e; } /* Function */
code span.im { } /* Import */
code span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */
code span.kw { color: #007020; font-weight: bold; } /* Keyword */
code span.op { color: #666666; } /* Operator */
code span.ot { color: #007020; } /* Other */
code span.pp { color: #bc7a00; } /* Preprocessor */
code span.sc { color: #4070a0; } /* SpecialChar */
code span.ss { color: #bb6688; } /* SpecialString */
code span.st { color: #4070a0; } /* String */
code span.va { color: #19177c; } /* Variable */
code span.vs { color: #4070a0; } /* VerbatimString */
code span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */
.display.math{display: block; text-align: center; margin: 0.5rem auto;}
</style>
<style type="text/css">@page {
size: A4;
margin: 18mm 16mm 16mm 16mm;
@bottom-center {
content: counter(page) " / " counter(pages);
font-family: "Malgun Gothic", sans-serif;
font-size: 9pt;
color: #999;
}
}
html { font-size: 11pt; }
body {
font-family: "Malgun Gothic", "Hancom Gothic", sans-serif;
color: #23272e;
line-height: 1.65;
max-width: 920px;
margin: 0 auto;
padding: 24px;
}
h1 {
font-size: 1.7rem;
color: #b45309;
border-bottom: 3px solid #f59e0b;
padding-bottom: 8px;
margin: 0 0 4px;
}
h2 {
font-size: 1.25rem;
color: #b45309;
border-bottom: 1px solid #e5d3b3;
padding-bottom: 5px;
margin-top: 1.6em;
}
h3 { font-size: 1.05rem; color: #92400e; margin-top: 1.1em; }
a { color: #b45309; }
hr { border: none; border-top: 1px solid #e2e2e2; margin: 1.6em 0; }
ul { padding-left: 1.25em; }
li { margin: 0.18em 0; }
strong { color: #1f2937; }
code {
font-family: "D2Coding", Consolas, monospace;
background: #f4f1ea;
border: 1px solid #e7e0d2;
border-radius: 3px;
padding: 0.5px 5px;
font-size: 0.92em;
}
table {
border-collapse: collapse;
width: 100%;
margin: 0.8em 0;
font-size: 0.95em;
}
th, td { border: 1px solid #d8d2c4; padding: 6px 10px; text-align: left; vertical-align: top; }
th { background: #fdf3df; color: #7c2d12; }
blockquote {
border-left: 4px solid #f59e0b;
margin: 0.8em 0;
padding: 0.2em 0 0.2em 14px;
color: #555;
background: #fffbf2;
}
h1, h2, h3 { break-after: avoid; }
</style>
<!--[if lt IE 9]>
<script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv-printshiv.min.js"></script>
<![endif]-->
</head>
<body>
<header id="title-block-header">
<h1 class="title">기술명세_GhiVideo_종합기술문서</h1>
</header>
<h1 id="ghivideo-종합-기술-명세서">GhiVideo 종합 기술 명세서</h1>
<blockquote>
<p><strong>목적</strong>: 본 문서는 (1) <strong>특허 출원/발명 신고</strong>의 기초 자료와 (2) <strong>후임 개발자 인계·업그레이드</strong> 자료를 겸한다. <strong>대상 시스템</strong>: GhiVideo — 시간축이 아니라 <strong>노선 위치(측점·체이니지)</strong> 를 1차 좌표로 삼아 드론 주행영상을 색인·탐색하고, 프레임별 카메라 자세로 측점·POI·선로중심선을 영상 위에 실시간 정합(geo-registration)하는 철도 주행영상 분석 플레이어. <strong>작성일</strong>: 2026-06-29 · 모든 소스 참조는 <code>[경로](경로#Lxx)</code> 형식(클릭 가능).</p>
</blockquote>
<hr />
<h2 id="0-문서-구성과-읽는-법">0. 문서 구성과 읽는 법</h2>
<table>
<thead>
<tr class="header">
<th></th>
<th>내용</th>
<th>주 독자</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>1</td>
<td>시스템 개요·아키텍처·데이터 흐름</td>
<td>전체</td>
</tr>
<tr class="even">
<td>2</td>
<td><strong>좌표·투영 정본 파이프라인</strong> (모든 정합의 기반)</td>
<td>전체</td>
</tr>
<tr class="odd">
<td>3</td>
<td>카메라 투영 &amp; 화각(FOV) 정합</td>
<td>개발/특허</td>
</tr>
<tr class="even">
<td>4</td>
<td>측점 표고 정합 (DEM-free) — 발명 B</td>
<td>특허</td>
</tr>
<tr class="odd">
<td>5</td>
<td>드론 자세 평활/보간 + 오버레이 렌더링</td>
<td>개발</td>
</tr>
<tr class="even">
<td>6</td>
<td>측점(체이니지) 시스템 &amp; 스테이션바</td>
<td>개발/특허</td>
</tr>
<tr class="odd">
<td>7</td>
<td>위치보정 역투영 — 발명 C</td>
<td>특허</td>
</tr>
<tr class="even">
<td>8</td>
<td>백엔드 스트리밍/FFmpeg·데이터·상태관리</td>
<td>개발</td>
</tr>
<tr class="odd">
<td>9</td>
<td>개발 타임라인(주요 의사결정)</td>
<td>전체</td>
</tr>
<tr class="even">
<td>10</td>
<td><strong>특허 관점 종합</strong>(인벤토리·후보·청구포인트·선행기술)</td>
<td>특허</td>
</tr>
<tr class="odd">
<td>11</td>
<td>검증 결과표</td>
<td>전체</td>
</tr>
<tr class="even">
<td>12</td>
<td>후임자 인계(데이터흐름·튜닝상수·빌드·업그레이드)</td>
<td>개발</td>
</tr>
<tr class="odd">
<td>13</td>
<td>한계 / 출원 전 확인사항</td>
<td>전체</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="1-시스템-개요--아키텍처">1. 시스템 개요 &amp; 아키텍처</h2>
<h3 id="11-핵심-아이디어">1.1 핵심 아이디어</h3>
<p>드론으로 촬영한 철도 선로 주행영상의 각 프레임에는 SRT 메타데이터로 <strong>드론 위치(lat/lon/고도)·자세(yaw/pitch/roll)·초점거리</strong>가 기록된다. 한편 철도 GIS에는 측점·POI·선로 중심선의 <strong>WGS84 좌표(+표고)</strong> 가 있다. 이 둘을 <strong>핀홀 카메라 투영</strong>으로 결합하면:</p>
<ol type="1">
<li><strong>순방향 투영</strong>&quot;이 GIS 지점이 지금 화면 어느 픽셀에 보이는가&quot; → 라벨/중심선/궤적 오버레이.</li>
<li><strong>역투영</strong>&quot;사용자가 화면에서 끈 픽셀은 어느 지리좌표인가&quot; → POI 위치/표고 보정.</li>
<li><strong>측점 색인</strong> — 드론 GPS를 측점 폴리라인에 투영해 영상 매 시점을 <strong>측점값(체이니지)</strong> 으로 환산 → 측점 기반 탐색/스크러빙.</li>
</ol>
<h3 id="12-데이터-흐름">1.2 데이터 흐름</h3>
<pre><code>[폴더 선택: base.csv(드론프레임) + building/측점·교량·터널·역사 CSV + route.json + KMZ]
│ geoData.loadFolderGeoData (인코딩 자동감지·병렬 파싱·KMZ 우선)
[geoStore] frames / stations / pois / structures / centerline / origin / routeMeta
├─▶ StationOverlay (영상 위 AR 오버레이)
│ precompute(가시성/겹침, requestIdleCallback) → RAF(연속포즈 재투영) → Canvas
│ geoProjection: WGS84→TM→ENU→카메라→핀홀
├─▶ StationBar (하단 측점 스크러버)
│ chainage: 드론GPS→측점폴리라인 수직투영→연속 체이니지
│ 이동거리/측점축, 방향색, 구조물 마커, 측점검색 순환
└─▶ VideoPlayer (Video.js + hls.js, 이중 재생경로, 프레임 정확도)</code></pre>
<h3 id="13-기술-스택">1.3 기술 스택</h3>
<ul>
<li><strong>프론트엔드</strong>: React 18 + TypeScript, Video.js 8.23 + @videojs/http-streaming, hls.js 1.6, Zustand(상태), Vite, Canvas 2D(오버레이), proj4(좌표변환), fflate(KMZ 압축해제).</li>
<li><strong>백엔드</strong>: Node.js 20 + Express, <code>child_process.spawn</code> FFmpeg 래퍼, Range Request 스트리밍, HLS 변환 + SSE 진행률.</li>
<li><strong>배포</strong>: PM2(<code>ghiVideo</code>, 포트 55000)가 <code>client/dist</code> 정적 서빙(빌드 결과물). 소스 변경 시 <code>npm run build -w client</code>(Node 20) 후 반영.</li>
</ul>
<hr />
<h2 id="2-좌표투영-정본-파이프라인--모든-정합의-기반">2. 좌표·투영 정본 파이프라인 (★ 모든 정합의 기반)</h2>
<blockquote>
<p>본 절을 <strong>정본</strong>으로 하고, 3~7장은 이를 참조한다. Python 레퍼런스 <code>advanced_tuner_v2.py</code>와 회전행렬·축정렬·정규화 공식이 1:1 동치로 이식되어 있다(<a href="client/src/utils/geoProjection.ts#L1">geoProjection.ts:1-12</a>).</p>
</blockquote>
<h3 id="21-5단계-변환">2.1 5단계 변환</h3>
<pre><code>[1] WGS84 (lat, lon, EL정표고)
│ proj4 EPSG:4326 → EPSG:5186 (한국 중부원점 TM)
[2] 월드 ENU [East(m), North(m), Up(m)] (East=X, North=Y)
│ rel = target_ENU (drone_ENU + offset)
[3] 카메라좌표 [Xc, Yc, Zc] (R_w2c = R_align · R_b2w(yaw,pitch,roll)ᵀ)
│ 핀홀 + focal/sensor
[4] 정규 이미지 좌표 [px, py] ∈ (0~1) (0.5 = 영상 중심)
│ object-fit:cover 변환
[5] 화면 px</code></pre>
<h3 id="22-단계별-수식코드">2.2 단계별 수식·코드</h3>
<p><strong>단계 1 — WGS84 → TM</strong> (<a href="client/src/utils/geoProjection.ts#L75">geoProjection.ts:75-85</a>)</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode ts"><code class="sourceCode typescript"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a>proj4<span class="op">.</span><span class="fu">defs</span>(<span class="st">&#39;EPSG:5186&#39;</span><span class="op">,</span> <span class="st">&#39;+proj=tmerc +lat_0=38 +lon_0=127 +k=1 +x_0=200000 +y_0=600000 +ellps=GRS80 +units=m +no_defs&#39;</span>)<span class="op">;</span></span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">latLonToTM</span>(lat<span class="op">,</span> lon) { const [e<span class="op">,</span> n] <span class="op">=</span> _toTM<span class="op">.</span><span class="fu">forward</span>([lon<span class="op">,</span> lat])<span class="op">;</span> return [e<span class="op">,</span> n]<span class="op">;</span> }</span></code></pre></div>
<p><strong>단계 2 — TM → ENU + 표고 datum 정합</strong> (<a href="client/src/utils/geoProjection.ts#L91">geoProjection.ts:91-97</a>, <a href="client/src/utils/geoProjection.ts#L263">263-271</a>). X/Y는 절대 TM, Z만 기준고도 대비 상대(상대벡터를 빼므로 무관). <strong>핵심: GIS 정표고(EL) + 지오이드고(N) → 타원체고</strong>로 변환해 드론 GPS 타원체고와 datum을 맞춘다.</p>
<div class="sourceCode" id="cb4"><pre class="sourceCode ts"><code class="sourceCode typescript"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a>const stEnu <span class="op">=</span> <span class="fu">geoToEnu</span>(targetLat<span class="op">,</span> targetLon<span class="op">,</span> targetAlt <span class="op">+</span> (params<span class="op">.</span><span class="at">geoidOffset</span> <span class="op">??</span> <span class="dv">0</span>)<span class="op">,</span> <span class="op">...</span>)<span class="op">;</span> <span class="co">// EL + N</span></span>
<span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a>const drEnuAdj <span class="op">=</span> [drEnu[<span class="dv">0</span>]<span class="op">+</span>offX<span class="op">,</span> drEnu[<span class="dv">1</span>]<span class="op">+</span>offY<span class="op">,</span> drEnu[<span class="dv">2</span>]<span class="op">+</span>offZ]<span class="op">;</span></span>
<span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a>const relEnu <span class="op">=</span> [stEnu[<span class="dv">0</span>]<span class="op">-</span>drEnuAdj[<span class="dv">0</span>]<span class="op">,</span> stEnu[<span class="dv">1</span>]<span class="op">-</span>drEnuAdj[<span class="dv">1</span>]<span class="op">,</span> stEnu[<span class="dv">2</span>]<span class="op">-</span>drEnuAdj[<span class="dv">2</span>]]<span class="op">;</span></span></code></pre></div>
<blockquote>
<p><strong>지오이드고 N</strong>: 대전지역 ≈ <strong>25.8m</strong>(= 84.244 16.853 41.59, KNGeoid 검증값). 초기 문서엔 25.449m로 기록된 바 있으나 본 명세는 25.8m로 통일(§11). 지역 상수이므로 타 지역 데이터셋은 재튜닝 필요.</p>
</blockquote>
<p><strong>단계 3 — ENU → 카메라좌표</strong> (<a href="client/src/utils/geoProjection.ts#L275">geoProjection.ts:275-295</a>). <code>R_b2w = Rz(yaw)·Rx(pitch)·Ry(roll)</code>, <code>R_w2c = R_align·R_b2wᵀ</code>, <code>R_align=[[1,0,0],[0,0,1],[0,1,0]]</code>. 행렬을 명시 생성하지 않고 전치(행=열)·부호치환으로 직접 계산:</p>
<div class="sourceCode" id="cb5"><pre class="sourceCode ts"><code class="sourceCode typescript"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">applyRw2c</span>(b2w<span class="op">,</span> rel) {</span>
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a> return {</span>
<span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a> Xc<span class="op">:</span> b2w[<span class="dv">0</span>][<span class="dv">0</span>]<span class="op">*</span>rel[<span class="dv">0</span>] <span class="op">+</span> b2w[<span class="dv">1</span>][<span class="dv">0</span>]<span class="op">*</span>rel[<span class="dv">1</span>] <span class="op">+</span> b2w[<span class="dv">2</span>][<span class="dv">0</span>]<span class="op">*</span>rel[<span class="dv">2</span>]<span class="op">,</span> <span class="co">// col0</span></span>
<span id="cb5-4"><a href="#cb5-4" aria-hidden="true" tabindex="-1"></a> Yc<span class="op">:</span> <span class="op">-</span>(b2w[<span class="dv">0</span>][<span class="dv">2</span>]<span class="op">*</span>rel[<span class="dv">0</span>] <span class="op">+</span> b2w[<span class="dv">1</span>][<span class="dv">2</span>]<span class="op">*</span>rel[<span class="dv">1</span>] <span class="op">+</span> b2w[<span class="dv">2</span>][<span class="dv">2</span>]<span class="op">*</span>rel[<span class="dv">2</span>])<span class="op">,</span> <span class="co">// -col2</span></span>
<span id="cb5-5"><a href="#cb5-5" aria-hidden="true" tabindex="-1"></a> Zc<span class="op">:</span> b2w[<span class="dv">0</span>][<span class="dv">1</span>]<span class="op">*</span>rel[<span class="dv">0</span>] <span class="op">+</span> b2w[<span class="dv">1</span>][<span class="dv">1</span>]<span class="op">*</span>rel[<span class="dv">1</span>] <span class="op">+</span> b2w[<span class="dv">2</span>][<span class="dv">1</span>]<span class="op">*</span>rel[<span class="dv">2</span>]<span class="op">,</span> <span class="co">// col1</span></span>
<span id="cb5-6"><a href="#cb5-6" aria-hidden="true" tabindex="-1"></a> }<span class="op">;</span></span>
<span id="cb5-7"><a href="#cb5-7" aria-hidden="true" tabindex="-1"></a>}</span></code></pre></div>
<p>규약: <strong>Zc&gt;0 = 카메라 정면</strong>(영상 깊이축), Xc=우, Yc=하. Zc≤0은 카메라 뒤 → null.</p>
<p><strong>단계 4 — 카메라좌표 → 정규 픽셀(핀홀)</strong> (<a href="client/src/utils/geoProjection.ts#L126">geoProjection.ts:126-137</a>). 영상 중심 = 0.5, 해상도 독립:</p>
<div class="sourceCode" id="cb6"><pre class="sourceCode ts"><code class="sourceCode typescript"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a>pxRaw <span class="op">=</span> (<span class="fl">0.5</span> <span class="op">+</span> cx0) <span class="op">+</span> (Xc<span class="op">/</span>Zc) <span class="op">*</span> (f <span class="op">/</span> sensorW)<span class="op">;</span> <span class="co">// 가로</span></span>
<span id="cb6-2"><a href="#cb6-2" aria-hidden="true" tabindex="-1"></a>pyRaw <span class="op">=</span> (<span class="fl">0.5</span> <span class="op">+</span> cy0) <span class="op">+</span> (Yc<span class="op">/</span>Zc) <span class="op">*</span> (f <span class="op">/</span> sensorH)<span class="op">;</span> <span class="co">// 세로</span></span></code></pre></div>
<p><strong>단계 5 — 정규 → 화면 px (object-fit:cover)</strong> (<a href="client/src/components/overlay/StationOverlay.tsx#L923">StationOverlay.tsx:923-933</a>). 영상 크롭 사각형에 맞춰 정합:</p>
<div class="sourceCode" id="cb7"><pre class="sourceCode ts"><code class="sourceCode typescript"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a>const s <span class="op">=</span> <span class="bu">Math</span><span class="op">.</span><span class="fu">max</span>(W <span class="op">/</span> vW<span class="op">,</span> H <span class="op">/</span> vH)<span class="op">;</span> <span class="co">// cover: 더 큰 배율(넘침=크롭)</span></span>
<span id="cb7-2"><a href="#cb7-2" aria-hidden="true" tabindex="-1"></a>dispW <span class="op">=</span> vW <span class="op">*</span> s<span class="op">;</span> dispH <span class="op">=</span> vH <span class="op">*</span> s<span class="op">;</span> offX <span class="op">=</span> (W <span class="op">-</span> dispW)<span class="op">/</span><span class="dv">2</span><span class="op">;</span> offY <span class="op">=</span> (H <span class="op">-</span> dispH)<span class="op">/</span><span class="dv">2</span><span class="op">;</span></span>
<span id="cb7-3"><a href="#cb7-3" aria-hidden="true" tabindex="-1"></a>const vx <span class="op">=</span> (nx) <span class="kw">=&gt;</span> offX <span class="op">+</span> nx <span class="op">*</span> dispW<span class="op">;</span> const vy <span class="op">=</span> (ny) <span class="kw">=&gt;</span> offY <span class="op">+</span> ny <span class="op">*</span> dispH<span class="op">;</span></span></code></pre></div>
<h3 id="23-두-투영-경로의-의도적-분리">2.3 두 투영 경로의 의도적 분리</h3>
<ul>
<li><strong>렌더(정밀)</strong>: <a href="client/src/utils/geoProjection.ts">geoProjection.ts</a> — proj4 EPSG:5186 TM + 회전행렬 + geoid/포커스 보정. (3~5장)</li>
<li><strong>검색(경량)</strong>: <a href="client/src/utils/geoSearch.ts">geoSearch.ts</a> — cos-lat 근사 ENU. &quot;현재 시야 내 POI&quot; 또는 &quot;특정 POI가 보이는 프레임 구간&quot; 산출용.</li>
</ul>
<p>ENU 월드원점은 <strong>첫 측점</strong>(없으면 첫 드론프레임, 없으면 중심선 첫 점)(<a href="client/src/utils/geoSearch.ts#L117">geoSearch.ts:117-128</a>).</p>
<hr />
<h2 id="3-카메라-투영--화각fov-정합">3. 카메라 투영 &amp; 화각(FOV) 정합</h2>
<blockquote>
<p>좌표 변환 파이프라인은 §2 참조. 본 절은 화각·내부표정 보정과 역투영을 다룬다.</p>
</blockquote>
<h3 id="31-focal--fov-관계">3.1 focal ↔︎ FOV 관계</h3>
<p>화각은 focal과 센서 치수로 결정된다(핀홀): <code>FOV = 2·atan(sensor / (2·focal))</code> (<a href="client/src/components/overlay/StationOverlay.tsx#L864">StationOverlay.tsx:864</a>). 기본값 <code>focal=24mm, sensorW=36mm(풀프레임 환산), sensorH=20.25mm(=36×9/16, 16:9)</code> (<a href="client/src/utils/geoProjection.ts#L48">geoProjection.ts:48-66</a>). 이 hFOV는 나침반/미니맵 시야부채꼴에도 사용.</p>
<h3 id="32-fovmode--세로-화각sensorh-역산-보정-">3.2 fovMode — 세로 화각(sensorH) 역산 보정 (★)</h3>
<p>라벨이 멀리/가까이에서 <strong>상하로 어긋날 때</strong>, 사용자가 POI 하나를 영상 속 실제 위치로 드래그하면 그 한 점의 대응으로 세로 화각을 역산한다. <strong>설계 핵심: 가로 focal(f/sensorW)은 건드리지 않고 sensorH만 역산</strong> → 빨간 중심선이 좌우로 틀어지지 않으면서 상하 배율만 교정.</p>
<p>투영식 <code>py = 0.5 + cy0 + (Yc/Zc)·(f/sH)</code> 를 sH에 대해 풀면 (<a href="client/src/components/overlay/StationOverlay.tsx#L1326">StationOverlay.tsx:1326-1344</a>):</p>
<div class="sourceCode" id="cb8"><pre class="sourceCode ts"><code class="sourceCode typescript"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a>const cc <span class="op">=</span> <span class="fu">toCameraCoords</span>(drone<span class="op">,</span> drag<span class="op">.</span><span class="at">lat</span><span class="op">,</span> drag<span class="op">.</span><span class="at">lon</span><span class="op">,</span> poiZ<span class="op">,</span> p<span class="op">,</span> wo)<span class="op">;</span></span>
<span id="cb8-2"><a href="#cb8-2" aria-hidden="true" tabindex="-1"></a>const b <span class="op">=</span> cc<span class="op">.</span><span class="at">Yc</span> <span class="op">/</span> cc<span class="op">.</span><span class="at">Zc</span><span class="op">;</span></span>
<span id="cb8-3"><a href="#cb8-3" aria-hidden="true" tabindex="-1"></a>const v <span class="op">=</span> pos<span class="op">.</span><span class="at">y</span> <span class="op">-</span> <span class="fl">0.5</span> <span class="op">-</span> (p<span class="op">.</span><span class="at">cy0</span> <span class="op">??</span> <span class="dv">0</span>)<span class="op">;</span></span>
<span id="cb8-4"><a href="#cb8-4" aria-hidden="true" tabindex="-1"></a>const sHNew <span class="op">=</span> (b <span class="op">*</span> p<span class="op">.</span><span class="at">focalLen</span>) <span class="op">/</span> v<span class="op">;</span> <span class="co">// sH = (Yc/Zc)·f / (py 0.5 cy0)</span></span>
<span id="cb8-5"><a href="#cb8-5" aria-hidden="true" tabindex="-1"></a><span class="fu">setParam</span>(<span class="st">&#39;sensorH&#39;</span><span class="op">,</span> <span class="bu">Math</span><span class="op">.</span><span class="fu">max</span>(<span class="dv">6</span><span class="op">,</span> <span class="bu">Math</span><span class="op">.</span><span class="fu">min</span>(<span class="dv">36</span><span class="op">,</span> sHNew)))<span class="op">;</span></span></code></pre></div>
<p>영상 세로 중심(v≈0)은 발산 → <code>|v|&lt;0.03</code>이면 거부하고 안내.</p>
<h3 id="33-핵심-함수">3.3 핵심 함수</h3>
<ul>
<li><strong><code>toCameraCoords</code></strong> (<a href="client/src/utils/geoProjection.ts#L301">geoProjection.ts:301-317</a>) — GIS점→카메라좌표. 부산물 <code>distH</code>(수평 직선거리), <code>fwd/side</code>(진행방향 앞/옆 성분, 비등방 거리필터용). <code>pixelFromCamera</code>와 분리한 이유: 중심선처럼 선분 단위 근거리 Zc 클리핑·보간 후 투영해야 하기 때문.</li>
<li><strong><code>pixelFromCamera</code></strong> (<a href="client/src/utils/geoProjection.ts#L126">geoProjection.ts:126-137</a>) — 카메라좌표→정규픽셀(클램프 없음).</li>
<li><strong><code>groundPointFromPixel</code></strong> (<a href="client/src/utils/geoProjection.ts#L218">geoProjection.ts:218-251</a>) — 화면픽셀→지면 lat/lon. 광선-수평면 교차:
<div class="sourceCode" id="cb9"><pre class="sourceCode ts"><code class="sourceCode typescript"><span id="cb9-1"><a href="#cb9-1" aria-hidden="true" tabindex="-1"></a>const t <span class="op">=</span> relUpTarget <span class="op">/</span> dir[<span class="dv">2</span>]<span class="op">;</span> <span class="co">// dir = R_w2cᵀ·((px0.5cx0)·sW/f, (py0.5cy0)·sH/f, 1)</span></span>
<span id="cb9-2"><a href="#cb9-2" aria-hidden="true" tabindex="-1"></a><span class="fu">if</span> (t <span class="op">&lt;=</span> <span class="dv">0</span>) return null<span class="op">;</span> <span class="co">// 카메라 뒤/위</span></span>
<span id="cb9-3"><a href="#cb9-3" aria-hidden="true" tabindex="-1"></a>const E <span class="op">=</span> drEnu[<span class="dv">0</span>]<span class="op">+</span>offX <span class="op">+</span> t<span class="op">*</span>dir[<span class="dv">0</span>]<span class="op">;</span> const N <span class="op">=</span> drEnu[<span class="dv">1</span>]<span class="op">+</span>offY <span class="op">+</span> t<span class="op">*</span>dir[<span class="dv">1</span>]<span class="op">;</span></span>
<span id="cb9-4"><a href="#cb9-4" aria-hidden="true" tabindex="-1"></a>const [lon<span class="op">,</span> lat] <span class="op">=</span> _toTM<span class="op">.</span><span class="fu">inverse</span>([E<span class="op">,</span> N])<span class="op">;</span></span></code></pre></div></li>
<li>관련 역투영 2종: <code>worldFromPixel</code>(슬랜트거리 유지 lat/lon/z 동시복원, <a href="client/src/utils/geoProjection.ts#L149">149-183</a>), <code>solveZForPixelY</code>(lat/lon 고정, py에 맞는 z만 1차식 역산, <a href="client/src/utils/geoProjection.ts#L194">194-211</a>). → 발명 C(§7).</li>
</ul>
<h3 id="34-보정값-영속화-데이터셋별">3.4 보정값 영속화 (데이터셋별)</h3>
<ul>
<li>카메라/내부표정·표시설정: <code>ghivideo:calib:{baseName}</code> (로드 1회, 저장 800ms debounce, <a href="client/src/components/overlay/StationOverlay.tsx#L473">StationOverlay.tsx:473-500</a>).</li>
<li>POI 위치/표고 override(DEM·드래그): <code>ghivideo:poiov:{baseName}</code> (폴더 파일 override 위에 localStorage 병합, <a href="client/src/components/overlay/StationOverlay.tsx#L502">502-524</a>).</li>
</ul>
<h3 id="35-설계-트레이드오프한계">3.5 설계 트레이드오프·한계</h3>
<ul>
<li><strong>렌즈 왜곡 미보정</strong>: 핀홀만 사용(방사/접선 왜곡 계수 없음). 분석 보조 라벨 정합엔 표준 정확도로 충분(가장자리 수 px 오차 가능).</li>
<li><strong>클라이언트 순수 계산</strong>: proj4만 의존, 서버 왕복 없이 60fps RAF에서 직접 투영.</li>
<li><strong>focal 분리 보정</strong>: 한 점 대응으로 sensorH 한 축만 역산(가로 정합 보존).</li>
<li><strong>POI 표고 가정</strong>: 실측 표고 없으면 최근접 선로 지면표고(또는 &quot;드론고도−이격&quot;) → §4·드래그·DEM 보정.</li>
</ul>
<hr />
<h2 id="4-측점-표고-정합-dem-free-영상정합--발명-b">4. 측점 표고 정합 (DEM-free 영상정합) — 발명 B</h2>
<h3 id="41-무엇을--왜">4.1 무엇을 / 왜</h3>
<p>표시할 시설물(POI)은 주소 기반이라 <strong>표고값이 없는</strong> 경우가 대부분이다. 표고가 없으면 부각(수직각) 산출 불가 → 종래는 지면을 평면으로 가정. 그러나 노선은 실제 기복(정표고 41.6m→57.5m)이 있어 평면 가정 시 라벨이 화면 밖/지면 아래로 이탈한다. 실측에서 잘못된 고도(≈0) 사용 시 <strong>부각오차 42.7°</strong>.</p>
<p>→ 별도 DEM 취득 없이, <strong>노선을 따라 실측된 측점의 정표고</strong>를 지면고도원으로 삼아 정합한다.</p>
<h3 id="42-구성독립항-필수요소">4.2 구성(독립항 필수요소)</h3>
<ol type="1">
<li><strong>측점 표고 수열 보유</strong> — 노선 따라 실측된 복수 측점의 (lat/lon + 정표고 EL). (실데이터: 측점 47개, 41.6→57.5m)</li>
<li><strong>최근접 측점 지면고도 채택</strong> — 표고 미상 대상에 가장 가까운 측점의 정표고를 지면고도로. 프레임 진행에 따라 최근접 측점이 바뀌어 <strong>지면고도가 노선 기복을 따라 갱신</strong>.</li>
<li><strong>지오이드 datum 정합</strong> — 정표고(EL) + 지오이드고(N) → 타원체고 → 드론 GPS와 동일 기준. <code>rel_up = (targetZ + N) droneAlt</code>.</li>
<li><strong>정합 투영</strong> — §2 파이프라인 수행.</li>
<li>(종속) <strong>대상별 표고 오프셋</strong>(poiZOffset) — 건물높이/지반차 보정, 측점·중심선엔 미적용.</li>
</ol>
<h3 id="43-검증값">4.3 검증값</h3>
<pre><code>타원체고 = 정표고(EL) + 지오이드고(N≈25.8m, 대전)
# 측점 157K900, frame0, 수평거리 91.3m
정표고≈0 → 부각 42.7° ❌
정표고 41.5 → 부각 25.1°
정표고+N≈67 → 부각 11.0° ✅ (드론 rel_alt 16.85m와 일치)</code></pre>
<ul>
<li><strong>데이터 함정(핵심)</strong>: <code>building/01)측점.csv</code>에 표고 컬럼이 둘 — <code>Z좌표</code>(로컬 ≈0) / <code>Z좌표_한국</code>(EPSG:5186 정표고 ≈41.5m). 기존 코드가 <code>Z좌표</code>(≈0)를 읽어 42.7° 오차 발생 → <code>Z좌표_한국</code> 우선으로 교정(<a href="client/src/utils/geoData.ts">geoData.ts 측점 파서</a>).</li>
<li><strong>효과</strong>: 부각오차 42.7°→<strong>11.0°</strong>, 구글어스 DEM 대비 <strong>0.2m 일치</strong>(2160p에서 ~6px). DEM 취득비용 불필요. 공개 DEM(SRTM 30m)보다 측량 측점이 더 정밀.</li>
</ul>
<hr />
<h2 id="5-드론-자세-평활보간--영상-오버레이-렌더링">5. 드론 자세 평활/보간 + 영상 오버레이 렌더링</h2>
<blockquote>
<p>두 본질 과제: <strong>(a) 자세/GPS 노이즈</strong>(단순평균은 회전 뭉갬→지연), <strong>(b) 렌더 부드러움 vs 계산비용</strong>(가시성 판정은 비싸고 60fps는 매 프레임 재투영 요구).</p>
</blockquote>
<h3 id="51-에지회전경계-보존-적응형-이동평균--smoothframe">5.1 에지(회전경계) 보존 적응형 이동평균 — <code>smoothFrame</code></h3>
<p><a href="client/src/components/overlay/StationOverlay.tsx#L577">StationOverlay.tsx:577-604</a>. 중심 <code>i</code>에서 좌우 최대 <code>halfWin</code>(기본 60fr≈2초)까지 평균하되, <strong>중심 yaw와 차이가 <code>YAW_EDGE=8°</code> 이내인 인접까지만 창 확장</strong>.</p>
<div class="sourceCode" id="cb11"><pre class="sourceCode ts"><code class="sourceCode typescript"><span id="cb11-1"><a href="#cb11-1" aria-hidden="true" tabindex="-1"></a>const YAW_EDGE <span class="op">=</span> <span class="dv">8</span><span class="op">;</span></span>
<span id="cb11-2"><a href="#cb11-2" aria-hidden="true" tabindex="-1"></a>const near <span class="op">=</span> (idx) <span class="kw">=&gt;</span> <span class="bu">Math</span><span class="op">.</span><span class="fu">abs</span>(((frames[idx]<span class="op">.</span><span class="at">yaw</span> <span class="op">-</span> yc <span class="op">+</span> <span class="dv">540</span>) <span class="op">%</span> <span class="dv">360</span>) <span class="op">-</span> <span class="dv">180</span>) <span class="op">&lt;=</span> YAW_EDGE<span class="op">;</span></span>
<span id="cb11-3"><a href="#cb11-3" aria-hidden="true" tabindex="-1"></a><span class="fu">while</span> (lo <span class="op">&gt;</span> loMin <span class="op">&amp;&amp;</span> <span class="fu">near</span>(lo <span class="op">-</span> <span class="dv">1</span>)) lo<span class="op">--;</span> <span class="co">// 회전경계에서 멈춤</span></span>
<span id="cb11-4"><a href="#cb11-4" aria-hidden="true" tabindex="-1"></a><span class="fu">while</span> (hi <span class="op">&lt;</span> hiMax <span class="op">&amp;&amp;</span> <span class="fu">near</span>(hi <span class="op">+</span> <span class="dv">1</span>)) hi<span class="op">++;</span></span>
<span id="cb11-5"><a href="#cb11-5" aria-hidden="true" tabindex="-1"></a><span class="co">// yaw는 원형평균: atan2(Σsin, Σcos)</span></span>
<span id="cb11-6"><a href="#cb11-6" aria-hidden="true" tabindex="-1"></a>yaw<span class="op">:</span> <span class="bu">Math</span><span class="op">.</span><span class="fu">atan2</span>(sinYaw <span class="op">/</span> n<span class="op">,</span> cosYaw <span class="op">/</span> n) <span class="op">*</span> <span class="dv">180</span> <span class="op">/</span> <span class="bu">Math</span><span class="op">.</span><span class="at">PI</span><span class="op">,</span></span></code></pre></div>
<ul>
<li>직선: 창 풀폭 → 노이즈 강하게 억제. 회전 중: 창 좁아져 <strong>즉시 추종</strong>(지연 없음). 회전 후: <strong>한 프레임씩</strong> 점진 재확장(튐 없음). 과거 &#39;변화율 기반 축소&#39;는 회전 종료 시 한꺼번에 재확장돼 회전 프레임이 다시 섞여 1회 튀던 부작용 → 경계보존으로 교체.</li>
</ul>
<h3 id="52-연속-프레임번호-보간-포즈--poseat">5.2 연속 프레임번호 보간 포즈 — <code>poseAt</code></h3>
<p><a href="client/src/components/overlay/StationOverlay.tsx#L670">StationOverlay.tsx:670-690</a>. 실수 프레임번호 <code>estFrame</code>에 대해 양 이웃의 평활포즈를 선형보간(이산 30fps 격자→60fps 연속). yaw는 <strong>최단각</strong> 보간:</p>
<div class="sourceCode" id="cb12"><pre class="sourceCode ts"><code class="sourceCode typescript"><span id="cb12-1"><a href="#cb12-1" aria-hidden="true" tabindex="-1"></a>let dy <span class="op">=</span> b<span class="op">.</span><span class="at">yaw</span> <span class="op">-</span> a<span class="op">.</span><span class="at">yaw</span><span class="op">;</span> dy <span class="op">=</span> ((dy <span class="op">+</span> <span class="dv">540</span>) <span class="op">%</span> <span class="dv">360</span>) <span class="op">-</span> <span class="dv">180</span><span class="op">;</span> <span class="co">// 최단각</span></span>
<span id="cb12-2"><a href="#cb12-2" aria-hidden="true" tabindex="-1"></a>return { <span class="op">...</span>a<span class="op">,</span> frame<span class="op">:</span> estFrame<span class="op">,</span> lat<span class="op">:</span><span class="fu">L</span>(a<span class="op">.</span><span class="at">lat</span><span class="op">,</span>b<span class="op">.</span><span class="at">lat</span>)<span class="op">,</span> <span class="op">...,</span> yaw<span class="op">:</span> a<span class="op">.</span><span class="at">yaw</span> <span class="op">+</span> dy<span class="op">*</span>frac }<span class="op">;</span></span></code></pre></div>
<p>O(log n) 이진탐색으로 매 RAF 호출 가능.</p>
<h3 id="53-속도적응-ema--이상치-거부--smoothstep-one-euro-사상">5.3 속도적응 EMA + 이상치 거부 — <code>smoothStep</code> (One-Euro 사상)</h3>
<p><a href="client/src/components/overlay/StationOverlay.tsx#L48">StationOverlay.tsx:48-61</a>. <strong>느릴 때(떨림)는 강하게 평활, 빠를 때(실제 팬)는 즉시 추종.</strong></p>
<div class="sourceCode" id="cb13"><pre class="sourceCode ts"><code class="sourceCode typescript"><span id="cb13-1"><a href="#cb13-1" aria-hidden="true" tabindex="-1"></a><span class="fu">if</span> (d <span class="op">&gt;</span> REJECT_DIST <span class="op">&amp;&amp;</span> prev<span class="op">.</span><span class="at">rej</span> <span class="op">&lt;</span> MAX_REJECT_FRAMES) <span class="co">// 0.12 초과 점프 = 이상치 → 위치 유지</span></span>
<span id="cb13-2"><a href="#cb13-2" aria-hidden="true" tabindex="-1"></a> return { x<span class="op">:</span> prev<span class="op">.</span><span class="at">x</span><span class="op">,</span> y<span class="op">:</span> prev<span class="op">.</span><span class="at">y</span><span class="op">,</span> rej<span class="op">:</span> prev<span class="op">.</span><span class="at">rej</span> <span class="op">+</span> <span class="dv">1</span><span class="op">,</span> <span class="op">...</span> }<span class="op">;</span></span>
<span id="cb13-3"><a href="#cb13-3" aria-hidden="true" tabindex="-1"></a>const vx <span class="op">=</span> prev<span class="op">.</span><span class="at">vx</span> <span class="op">+</span> (dx <span class="op">-</span> prev<span class="op">.</span><span class="at">vx</span>) <span class="op">*</span> SMOOTH_VEL_BETA<span class="op">;</span> <span class="co">// β=0.25 속도평활(떨림 왕복은 상쇄)</span></span>
<span id="cb13-4"><a href="#cb13-4" aria-hidden="true" tabindex="-1"></a>const a <span class="op">=</span> <span class="bu">Math</span><span class="op">.</span><span class="fu">min</span>(maxAlpha<span class="op">,</span> minAlpha <span class="op">+</span> (maxAlpha <span class="op">-</span> minAlpha) <span class="op">*</span> <span class="bu">Math</span><span class="op">.</span><span class="fu">min</span>(<span class="dv">1</span><span class="op">,</span> speed<span class="op">/</span>speedRef))<span class="op">;</span></span>
<span id="cb13-5"><a href="#cb13-5" aria-hidden="true" tabindex="-1"></a>return { x<span class="op">:</span> prev<span class="op">.</span><span class="at">x</span> <span class="op">+</span> dx<span class="op">*</span>a<span class="op">,</span> y<span class="op">:</span> prev<span class="op">.</span><span class="at">y</span> <span class="op">+</span> dy<span class="op">*</span>a<span class="op">,</span> <span class="op">...</span> }<span class="op">;</span></span></code></pre></div>
<ul>
<li>이상치 거부에 <strong>연속거부 한계(MAX_REJECT_FRAMES=8)</strong> → 시크/재등장 같은 진짜 큰 이동에 영구 고착 방지.</li>
<li>추가로 <strong>픽셀 히스테리시스</strong>(0.75px 이상일 때만 정수좌표 갱신)로 저속 반올림 깜빡임 제거(<a href="client/src/components/overlay/StationOverlay.tsx#L1048">1048</a>).</li>
</ul>
<h3 id="54-라벨-사전계산--겹침-억제">5.4 라벨 사전계산 + 겹침 억제</h3>
<ul>
<li><strong><code>startLabelPrecompute</code></strong> (<a href="client/src/components/overlay/StationOverlay.tsx#L692">StationOverlay.tsx:692-746</a>): 전 프레임의 <strong>가시집합·겹침해소 결과</strong><code>Map&lt;frameNum, LabelCache&gt;</code>에 사전계산. <strong>화면좌표가 아니라 월드좌표(lat/lon/z)를 저장</strong> → RAF가 매 프레임 연속포즈로 재투영. <code>requestIdleCallback</code> 200프레임 청크, 취소토큰(<code>precomputeIdRef</code>), <strong>현재 프레임부터 순환 처리</strong>(재생 즉시 라벨), 첫 청크 즉시 노출, 재계산 시 옛 맵 복사.</li>
<li><strong>겹침 억제</strong> (<a href="client/src/components/overlay/StationOverlay.tsx#L755">755-784</a>): 임계 가로 <code>POI_MERGE_X=0.10</code>/세로 <code>0.035</code>(비등방, 텍스트가 가로로 긴 특성). 우선순위 ①철도역/역사 최우선 ②근접거리. <strong>진행방향 변형 우선</strong>: 상/하행 형제 겹침 시 노선 방향((상)/(하)) 변형을 남김.</li>
</ul>
<h3 id="55-raf-렌더-루프--성능-설계">5.5 RAF 렌더 루프 &amp; 성능 설계</h3>
<p>매 프레임 <strong>계산 없이 조회+투영+드로잉만</strong>. 라이브 시간은 VideoPlayer <code>smoothTimeRef</code>(단조 벽시계 보간, 일시정지·시크·배속 보정)에서 받음.</p>
<table>
<thead>
<tr class="header">
<th>작업</th>
<th>비용</th>
<th>배치</th>
<th>빈도</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>가시성/겹침 판정</td>
<td>높음</td>
<td>precompute</td>
<td>1회(+설정변경 debounce), idle 청크</td>
</tr>
<tr class="even">
<td>좌표 투영</td>
<td>중간</td>
<td>RAF</td>
<td>매 프레임</td>
</tr>
<tr class="odd">
<td>평활/EMA/히스테리시스</td>
<td>낮음</td>
<td>RAF</td>
<td>매 프레임</td>
</tr>
<tr class="even">
<td>팝업 가시집합 setState</td>
<td>리렌더</td>
<td>setInterval 150ms</td>
<td>변화 시만</td>
</tr>
</tbody>
</table>
<p><strong>핵심 = &quot;무엇을 그릴지&quot;(precompute) ↔︎ &quot;어디에 그릴지&quot;(RAF) 분리</strong>로 단일스레드 60fps 달성. 중심선/드론궤적은 <code>buildLines</code>로 매 프레임 직접 투영(근거리 Zc 클리핑·outcode 컬링). 나침반은 별도 ±3fr 평활 yaw + 360° 누적 언랩.</p>
<hr />
<h2 id="6-측점체이니지-시스템--스테이션바">6. 측점(체이니지) 시스템 &amp; 스테이션바</h2>
<h3 id="61-연속-체이니지-수직투영-핵심-①">6.1 연속 체이니지 수직투영 (핵심 ①)</h3>
<p>드론 한 프레임의 위경도를 <strong>측점 폴리라인에 수직 투영</strong>해 100m 양자화가 아닌 <strong>10m 해상도 연속 측점값</strong>을 얻는다(부산물: 선로 수직 이격 offsetM).</p>
<ul>
<li><strong>폴리라인 구축</strong> <code>buildChainLine</code> (<a href="client/src/utils/chainage.ts#L22">chainage.ts:22-32</a>): 측점값 오름차순 정렬, 평면투영 <code>x=lon·k, y=lat·111000</code> (k=cos(lat0)·111000).</li>
<li><strong>점-선분 투영</strong> <code>projectToChain</code> (<a href="client/src/utils/chainage.ts#L35">chainage.ts:35-50</a>):
<div class="sourceCode" id="cb14"><pre class="sourceCode ts"><code class="sourceCode typescript"><span id="cb14-1"><a href="#cb14-1" aria-hidden="true" tabindex="-1"></a>const t <span class="op">=</span> L2<span class="op">===</span><span class="dv">0</span><span class="op">?</span><span class="dv">0</span><span class="op">:</span><span class="bu">Math</span><span class="op">.</span><span class="fu">max</span>(<span class="dv">0</span><span class="op">,</span><span class="bu">Math</span><span class="op">.</span><span class="fu">min</span>(<span class="dv">1</span><span class="op">,</span> ((px<span class="op">-</span>a<span class="op">.</span><span class="at">x</span>)<span class="op">*</span>dx <span class="op">+</span> (py<span class="op">-</span>a<span class="op">.</span><span class="at">y</span>)<span class="op">*</span>dy)<span class="op">/</span>L2))<span class="op">;</span></span>
<span id="cb14-2"><a href="#cb14-2" aria-hidden="true" tabindex="-1"></a>const d <span class="op">=</span> (px<span class="op">-</span>cx)<span class="op">**</span><span class="dv">2</span> <span class="op">+</span> (py<span class="op">-</span>cy)<span class="op">**</span><span class="dv">2</span><span class="op">;</span></span>
<span id="cb14-3"><a href="#cb14-3" aria-hidden="true" tabindex="-1"></a><span class="fu">if</span> (d <span class="op">&lt;</span> bestD) { bestD <span class="op">=</span> d<span class="op">;</span> bestKm <span class="op">=</span> a<span class="op">.</span><span class="at">km</span> <span class="op">+</span> (b<span class="op">.</span><span class="at">km</span><span class="op">-</span>a<span class="op">.</span><span class="at">km</span>)<span class="op">*</span>t<span class="op">;</span> } <span class="co">// 최근접 선분 위 km 보간</span></span></code></pre></div></li>
<li><strong>왜 연속 투영인가</strong>: 최근접 측점점(100m 양자화)은 값이 계단처럼 튀고 두 측점 중간에서 전진/후진 전환 시점이 어긋난다. 수직투영은 선분 보간으로 매 프레임 부드럽게 변해 <strong>방향 전환점(극값) 타이밍이 실제 주행과 일치</strong>.</li>
</ul>
<h3 id="62-이중-측점값-배지용-km-vs-방향용-chain">6.2 이중 측점값: 배지용 <code>km</code> vs 방향용 <code>chain</code></h3>
<p>프레임별 precompute(<a href="client/src/stationbar/StationBar.tsx#L233">StationBar.tsx:233-246</a>): <code>km</code>=최근접 측점점(100m, 배지/폴백), <code>chain</code>=연속투영(10m, 방향/축/탐지). <code>videoFps</code>는 실데이터(마지막 프레임/재생시간)에서 유도, 미비 시 <code>30000/1001</code> 폴백.</p>
<h3 id="63-축-설계-시간축이-아닌-위치축-핵심-②">6.3 축 설계: 시간축이 아닌 위치축 (핵심 ②)</h3>
<p>선로 점검 영상은 <strong>호버(정지비행)</strong><strong>전·후진 반복</strong> 특성이 있어 시간축이 부적합하다(호버 시 커서가 위치와 어긋남).</p>
<ul>
<li><strong>이동거리축</strong> (<a href="client/src/stationbar/StationBar.tsx#L250">StationBar.tsx:250-271</a>): 연속 측점값을 ±8fr 평활 후 프레임간 <strong>절대 변화량 누적</strong>(<code>cum += |sm[i]sm[i1]|</code>)을 정규화(<code>frac</code>, 단조 비감소). 호버=frac 정체(커서 정지), 이동=frac 증가. 방향은 색 리본으로 직교 분리.</li>
<li><strong>측점축</strong>(후속 채택안, <a href="docs/history/2026-06-26_1520_스테이션바-측점축-전환.md">history 2026-06-26_1520</a>): x축을 <strong>측점값 비례</strong>로 두고, 축 범위를 역 POI 첫/끝으로 <strong>노선 고정</strong>(영상 무관 절대 측점) → 어떤 데이터로 재생하든 같은 구조물이 같은 위치/측점에 표시(회차 간 정렬).</li>
<li>시간↔︎축 양방향 변환: <code>cumFracAtTime</code>/<code>timeAtFrac</code>(또는 <code>chainAtTime</code>/<code>pxAtChain</code>)를 이진탐색+보간으로. 커서·구간색·구조물 마커가 <strong>동일 매핑 공유</strong>. 라이브 커서는 React 리렌더 없이 RAF에서 CSS 변수(<code>--pos-px</code>)만 갱신(<a href="client/src/stationbar/StationBar.tsx#L771">771-786</a>).</li>
</ul>
<h3 id="64-방향-판정--방향색-트랙">6.4 방향 판정 &amp; 방향색 트랙</h3>
<p>평활 측점값 추세를 극값추적 + <strong>히스테리시스 HYST=10m</strong>로 방향 전환점 확정(<a href="client/src/stationbar/StationBar.tsx#L395">395-458</a>). <code>forwardDir = arrChain≥depChain?1:1</code>. 정방향=주황 음영, 역방향=청록. 이동거리/측점축은 구간 px가 단조라 <code>linear-gradient</code> 1장으로 경량 처리(<a href="client/src/stationbar/StationBar.tsx#L686">686-726</a>).</p>
<h3 id="65-구조물-마커--좌표측점-이중-탐지--kmexists-핵심-③">6.5 구조물 마커 — 좌표/측점 이중 탐지 &amp; <code>kmExists</code> (핵심 ③)</h3>
<p>두 통과 탐지기:</p>
<ul>
<li><strong><code>pxPassesTo(lat,lon,off)</code></strong> (<a href="client/src/stationbar/StationBar.tsx#L489">489-534</a>) — 좌표 근접. 임계 <code>TH=max(100, gmin·3)</code>.</li>
<li><strong><code>pxPassesAtMileage(targetM)</code></strong> (<a href="client/src/stationbar/StationBar.tsx#L537">537-559</a>) — 연속 체이니지가 <code>TH=stationTolerance(기본 20m)</code> 이내.</li>
</ul>
<p>탐지 우선순위(단락평가): ①명시 측점값 ②좌표를 측점선 투영→측점기준(통과순서 정확) ③이름매칭 POI 투영 ④좌표근접 폴백 … (<a href="client/src/stationbar/StationBar.tsx#L612">612-639</a>).</p>
<p><strong>역사 합집합 보강</strong>: 종점/조차장은 측점선 투영이 불안정해 재진입을 놓침(예 대전조차장 162k080). 단락평가 탓에 한 통과를 찾으면 좌표근접이 안 돌아 누락 → <strong>역사에 한해 좌표근접 통과를 합집합으로 합치되 24px 임계 중복제거</strong>(<a href="client/src/stationbar/StationBar.tsx#L629">629-634</a>).</p>
<p><strong><code>kmExists</code> (★)</strong>: 좌표 반경만으로 잡힌 통과는 실제 측점값이 그 위치에 없을 수 있다.</p>
<div class="sourceCode" id="cb15"><pre class="sourceCode ts"><code class="sourceCode typescript"><span id="cb15-1"><a href="#cb15-1" aria-hidden="true" tabindex="-1"></a>const kmExists <span class="op">=</span> stationKmVal <span class="op">==</span> null <span class="op">||</span> <span class="bu">Math</span><span class="op">.</span><span class="fu">abs</span>(p<span class="op">.</span><span class="at">km</span> <span class="op">-</span> stationKmVal) <span class="op">&lt;=</span> existTol<span class="op">;</span> <span class="co">// existTol = stationTolerance</span></span></code></pre></div>
<p>(<a href="client/src/stationbar/StationBar.tsx#L648">StationBar.tsx:648</a>) <code>false</code><strong>측점값 라벨·측점검색에서 제외하되 마커(동그라미)는 유지</strong>&quot;위치 표시&quot;&quot;측점 단정&quot;의 분리.</p>
<p><strong>종점 미도착</strong>: <code>endGapPx=min(폭·0.15, (gapM/lenM)·폭)</code>, <code>placeUnreachedTerminal</code>이 최우측 역사를 트랙 끝에 미도착(속 빈 링)으로 고정(<a href="client/src/stationbar/StationBar.tsx#L303">303-308</a>, <a href="client/src/stationbar/StationBar.tsx#L574">574-586</a>).</p>
<h3 id="66-측점검색-순환--handlejumptomileage">6.6 측점검색 순환 — <code>handleJumpToMileage</code></h3>
<p><a href="client/src/stationbar/StationBar.tsx#L850">StationBar.tsx:850-916</a>. 측점값 입력 시 여러 통과를 <strong>통과방향 순환</strong>:</p>
<ol type="1">
<li>후보 <strong>1순위</strong>: 화면 마커(<code>structureMarks</code>) 중 <code>kmExists!==false</code> 이고 표시 10m값 일치 → px→<code>timeAtFrac</code> 역변환. (좌표근접 통과 포함 → 모든 동그라미 도달)</li>
<li><strong>2순위</strong>: 마커 없으면 chain 연속구간, 최종 전역 최근접 폴백.</li>
<li>정렬 + 0.3s 중복제거 → baseT 기준 다음 통과(끝→처음 순환). <code>jumpRef</code>로 연속검색 판정. Enter 후 입력값 유지, blur 시 삭제(<a href="client/src/stationbar/components/PlaybackControls/PlaybackControls.tsx">PlaybackControls.tsx</a>).</li>
</ol>
<h3 id="67-타임라인-보조-기술-timelinetsx">6.7 타임라인 보조 기술 (<a href="client/src/stationbar/components/Timeline/Timeline.tsx">Timeline.tsx</a>)</h3>
<ul>
<li><strong>동일항목 브래킷 클러스터</strong>: 동명 다통과를 거리 무관 한 묶음으로(드롭+수평 점선) 중앙 라벨 1개(<a href="client/src/stationbar/components/Timeline/Timeline.tsx#L143">143-172</a>).</li>
<li><strong>라벨 2줄 균등분할</strong> <code>splitStructLabel</code>: CJK 1.0/기타 0.55폭, 괄호그룹은 통째로 아랫줄(<a href="client/src/stationbar/components/Timeline/Timeline.tsx#L30">30-49</a>).</li>
<li><strong>측점값 라벨 겹침 숨김</strong> <code>showMileage</code>: 직전 표시와 <code>(lastW+w)/2</code> 미만이면 숨김, <code>kmExists===false</code>는 애초 생략(<a href="client/src/stationbar/components/Timeline/Timeline.tsx#L117">117-137</a>).</li>
<li><strong>아이콘 passed 전환</strong>: upcoming/passed 두 PNG를 opacity로 전환(무깜빡임), 전 상태 사전로드.</li>
</ul>
<hr />
<h2 id="7-위치보정-역투영-단일-제스처--발명-c">7. 위치보정 역투영 (단일 제스처) — 발명 C</h2>
<h3 id="71-무엇을--왜">7.1 무엇을 / 왜</h3>
<p>지오코딩 POI는 ±수십 m 오차. 2D 화면 이동만으로는 카메라로부터의 거리(깊이)를 결정 못함(깊이 모호성). 종래는 수평/높이를 따로 조정하거나 깊이맵(LiDAR) 요구. → <strong>단일 드래그로 수평·수직(3D) 동시 보정, 깊이맵 불요.</strong></p>
<h3 id="72-구성독립항-필수요소">7.2 구성(독립항 필수요소)</h3>
<ol type="1">
<li><strong>슬랜트거리 보존</strong> — 드래그 개시 시점의 카메라–객체 슬랜트거리(range)를 고정 → 깊이 모호성 해소 핵심 구속.</li>
<li><strong>역투영</strong> — 드롭 픽셀을 카메라 방향벡터로 환산 후 보존 거리만큼 투사.</li>
<li><strong>월드좌표 복원</strong> — 자세 회전·평행이동 역변환으로 lat/lon/z 복원(지오이드 역환원). 한 번의 드래그로 수평·수직 동시.</li>
<li><strong>영속·재적용</strong> — 복원 좌표를 객체 식별자(title)에 연계해 <code>&lt;base&gt;_poi_overrides.json</code> + <code>ghivideo:poiov:&lt;baseName&gt;</code> 저장.</li>
<li>(종속) <strong>가시영역 한정</strong> — 전방(Zc&gt;0)만 보정.</li>
</ol>
<div class="sourceCode" id="cb16"><pre class="sourceCode ts"><code class="sourceCode typescript"><span id="cb16-1"><a href="#cb16-1" aria-hidden="true" tabindex="-1"></a>dir_cam <span class="fu"></span> ((px0<span class="op">.</span><span class="dv">5</span>cx0)·sW<span class="op">/</span>f<span class="op">,</span> (py0<span class="op">.</span><span class="dv">5</span>cy0)·sH<span class="op">/</span>f<span class="op">,</span> <span class="dv">1</span>)</span>
<span id="cb16-2"><a href="#cb16-2" aria-hidden="true" tabindex="-1"></a>cam <span class="op">=</span> dir_cam<span class="op">/|</span>dir_cam<span class="op">|</span> · range <span class="co">// ★거리 보존(드래그 시작값)</span></span>
<span id="cb16-3"><a href="#cb16-3" aria-hidden="true" tabindex="-1"></a>rel <span class="op">=</span> R_w2cᵀ · cam <span class="co">// 회전 역변환</span></span>
<span id="cb16-4"><a href="#cb16-4" aria-hidden="true" tabindex="-1"></a>z <span class="op">=</span> droneAlt <span class="op">+</span> offZ <span class="op">+</span> rel<span class="op">.</span><span class="at">up</span> N_geoid <span class="co">// 정표고 복원</span></span>
<span id="cb16-5"><a href="#cb16-5" aria-hidden="true" tabindex="-1"></a>(lat<span class="op">,</span>lon) <span class="op">=</span> <span class="fu">TM⁻¹</span>(droneE<span class="op">+</span>offX<span class="op">+</span>rel<span class="op">.</span><span class="at">x</span><span class="op">,</span> droneN<span class="op">+</span>offY<span class="op">+</span>rel<span class="op">.</span><span class="at">y</span>)</span></code></pre></div>
<p><strong>변형 — z 전용 역산</strong> <code>solveZForPixelY</code>: lat/lon 고정, 드롭 세로위치로 z만 1차식 역산. 왕복검증 z=42→py→42.000.</p>
<h3 id="73-효과">7.3 효과</h3>
<p>2D 드래그 1회로 3D 보정(단계 절반), 역투영 왕복오차 ≈0, 깊이맵 불요, 파일+localStorage 재현성. <strong>&quot;depth map 없이 거리보존&quot;</strong> 을 명시해 US9188444B2와 구별(§10).</p>
<hr />
<h2 id="8-백엔드-스트리밍ffmpeg--데이터-파이프라인--상태관리--플레이어">8. 백엔드 스트리밍/FFmpeg · 데이터 파이프라인 · 상태관리 · 플레이어</h2>
<h3 id="81-대용량2gb-재생--이중-경로">8.1 대용량(2GB+) 재생 — 이중 경로</h3>
<ul>
<li><strong>로컬</strong>(<a href="client/src/hooks/useVideoPlayer.ts#L80">useVideoPlayer.ts:80-88</a>): <code>URL.createObjectURL</code> Blob URL 직접 재생(업로드/변환 없음), 교체 시 <code>revokeObjectURL</code>(누수 방지). <strong>현 운영 주 경로.</strong></li>
<li><strong>서버</strong>: Range Request 즉시 재생, 비표준 코덱(HEVC 등)이면 HLS 자동 전환(<a href="client/src/hooks/useVideoPlayer.ts#L15">15-17</a>). (서버/HLS 경로는 보존되나 UI는 <code>SHOW_TOOLBAR=false</code>로 숨김.)</li>
</ul>
<h3 id="82-range-request-streamingts52-77">8.2 Range Request (<a href="server/src/services/streaming.ts#L52">streaming.ts:52-77</a>)</h3>
<p>핵심: 한 응답에 전체를 싣지 않음. 끝값 없으면 <code>start+10MB1</code>. <code>pipeline()</code>(백프레셔/에러 자동), <code>highWaterMark:1MB</code>, <code>Accept-Ranges:bytes</code>, path traversal 방어(<code>resolveVideoPath</code>). JS Number(2^531)로 4GB+ 안전(BigInt 불요). 멀티레인지 미지원.</p>
<h3 id="83-hls-변환--sse-hlsts">8.3 HLS 변환 + SSE (<a href="server/src/routes/hls.ts">hls.ts</a>)</h3>
<p>멱등(디스크에 <code>index.m3u8</code> 있으면 즉시 done), 코덱 분기(H.264 <code>-c copy</code> 수 초 / 그 외 libx264), <code>/progress</code> SSE로 stderr <code>time=</code> 파싱 푸시. hls.js 필수: <code>backBufferLength:30</code>(기본 Infinity면 장시간 재생 메모리 누수→크래시), maxBufferLength 30, enableWorker(<a href="client/src/hooks/useVideoPlayer.ts#L7">useVideoPlayer.ts:7-13</a>).</p>
<h3 id="84-ffmpeg-spawn-래퍼-ffmpegts">8.4 FFmpeg spawn 래퍼 (<a href="server/src/services/ffmpeg.ts">ffmpeg.ts</a>)</h3>
<p><code>-y</code> auto, stderr <code>time=HH:MM:SS.cc</code> 정규식 진행률, exit≠0 시 stderr 끝 500자 reject. 프레임 추출은 <code>-accurate_seek -ss {t} -i {f} -frames:v 1</code>(플래그 순서 = 입력단 정확 시킹, <a href="server/src/routes/frame.ts#L37">frame.ts:37-44</a>).</p>
<h3 id="85-데이터-파이프라인-geodatats">8.5 데이터 파이프라인 (<a href="client/src/utils/geoData.ts">geoData.ts</a>)</h3>
<p><code>&lt;input webkitdirectory&gt;</code>/드래그로 받은 <code>File[]</code>을 병렬 파싱(<code>Promise.all</code>). <strong>인코딩 자동감지</strong>(BOM→UTF-8/EUC-KR), <strong>헤더명 우선+위치 폴백</strong>(<code>makeFieldIndexer</code>), 좌표 유효성 게이트(lat 33~39/lon 124~132), KMZ 우선(있으면 CSV POI/구조물 미사용, fflate unzipSync).</p>
<table>
<thead>
<tr class="header">
<th>소스</th>
<th>파서</th>
<th>대상</th>
<th>매핑</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>&lt;base&gt;.csv</code></td>
<td>parseDroneFrames</td>
<td>DroneFrame[]</td>
<td>frame_cnt, lat/lon/alt, yaw/pitch/roll, focal</td>
</tr>
<tr class="even">
<td><code>building/01)측점.csv</code></td>
<td>parseStations</td>
<td>station + DirectionChange</td>
<td>측점→title, <strong>Z좌표_한국(정표고) 우선</strong>, 비고→방향전환</td>
</tr>
<tr class="odd">
<td><code>&lt;base&gt;_POI.csv</code> / <code>02)지장물.csv</code></td>
<td>parsePois</td>
<td>poi</td>
<td>category_clean/명칭, lat/lon</td>
</tr>
<tr class="even">
<td><code>02)지장물_역사.csv</code></td>
<td>parseHistoricStations</td>
<td>역사 구조물</td>
<td>명칭→name(스테이션바 마커)</td>
</tr>
<tr class="odd">
<td><code>03)교량/04)터널/06)구교.csv</code></td>
<td>parseStructures</td>
<td>bridge/tunnel</td>
<td>구분/시설물명, 연장→lengthM, 시설종별→grade</td>
</tr>
<tr class="even">
<td><code>05)출입문번호.csv</code></td>
<td>parseAccessDoors</td>
<td>출입문 poi</td>
<td>출입문번호→title</td>
</tr>
<tr class="odd">
<td><code>*.kmz/kml</code></td>
<td>parseKmz</td>
<td>poi+구조물</td>
<td>Placemark + description표→props</td>
</tr>
<tr class="even">
<td><code>route.json</code></td>
<td>mergeStructures</td>
<td>보정</td>
<td>station/offset/이정 override</td>
</tr>
</tbody>
</table>
<p>중심선은 v2.0에 별도 파일이 없어 측점을 측점값 순 정렬한 폴리라인으로 합성.</p>
<h3 id="86-상태관리">8.6 상태관리</h3>
<ul>
<li><strong>geoStore</strong>(메모리, persist 없음): frames/stations/pois/structures/centerline/origin/routeMeta. <code>basePois</code>(원본) vs <code>pois</code>(override 적용) 분리로 원본 불변성(<a href="client/src/store/geoStore.ts#L96">geoStore.ts:96-110</a>).</li>
<li><strong>settingsStore</strong>(localStorage <code>ghivideo.settings</code>): 시설종별 필터, 표시 토글들. 커스텀 <code>merge</code>로 스키마 진화 대비. <code>isGradeVisible</code> 공유 규칙.</li>
</ul>
<h3 id="87-플레이어-통합--프레임-정확도">8.7 플레이어 통합 &amp; 프레임 정확도</h3>
<ul>
<li>Video.js <code>data-vjs-player</code> + Strict Mode 가드, <code>controls:false</code>(측점바로 대체), <code>fill:true</code>.</li>
<li><strong>FPS 감지</strong>(<a href="client/src/hooks/useFrameStep.ts#L21">useFrameStep.ts:21-49</a>): <code>requestVideoFrameCallback</code>, 첫 콜백은 시작점만 기록(31fps 오감지 방지), 연속 2회 일치 확정. 스텝은 <code>1/fps</code>(누적 금지=드리프트 방지).</li>
<li><strong>이원 fps</strong>: 스텝 이동=실측 fps, 프레임번호 표시=29.97 고정(SRT 기준). 의도된 분리.</li>
<li><strong>매끄러운 커서</strong>: rAF 단조 벽시계 보간 <code>smoothTimeRef</code>(매 프레임, transform) + <code>smoothTime</code>(throttle, 배지) 분리.</li>
</ul>
<hr />
<h2 id="9-개발-타임라인-주요-기술-의사결정">9. 개발 타임라인 (주요 기술 의사결정)</h2>
<table>
<thead>
<tr class="header">
<th>시기</th>
<th>핵심 결정/구현</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>2026-04-01</td>
<td>labelMap 사전계산 + requestIdleCallback + RAF, ±10fr 이동평균(yaw sin/cos), 30→60fps 보간</td>
</tr>
<tr class="even">
<td>2026-04-02</td>
<td><strong>투영 파이프라인 정립</strong>: EPSG:5186 TM, 핀홀(Rz·Rx·Ry), 타원체고=정표고+지오이드, Cohen-Sutherland 클리핑</td>
</tr>
<tr class="odd">
<td>2026-06-19</td>
<td><strong>v1→v2 데이터 형상 전환</strong>(실측 폴더), 인코딩 자동감지, 측점 비고→방향전환 추출, 시설종별 필터</td>
</tr>
<tr class="even">
<td>2026-06-22</td>
<td><strong>측점 표고 컬럼 교정(Z좌표_한국) → 부각 42.7°→11°</strong>(발명 B). 역투영 드래그 보정+override(발명 C 최초). title-키 보간+이상치 거부. 비등방 거리필터</td>
</tr>
<tr class="odd">
<td>2026-06-23</td>
<td>에지보존 평활(YAW_EDGE=8°). <strong>글로벌+KIPRIS 선행기술 조사</strong> → B·C 신규성 확인</td>
</tr>
<tr class="even">
<td>2026-06-24</td>
<td><strong>1클릭 FOV 보정(→sensorH 역산)</strong>, object-fit:cover 정렬, 라벨 월드좌표 저장+RAF 재투영, One-Euro EMA, KMZ 직접 로딩</td>
</tr>
<tr class="odd">
<td>2026-06-25~26</td>
<td>종점역 누락 수정, 양끝 역 고정, 라벨 2줄분할, <strong>동일항목 브래킷</strong>, <strong>거리축→측점축 전환</strong></td>
</tr>
<tr class="even">
<td>2026-06-29</td>
<td><strong>좌표/측점 이중탐지+합집합 보강</strong>, 측점값 라벨 겹침숨김, <strong>측점검색 순환+마커기준 전환+kmExists</strong>, UI 통합(토글 재생바 이동, 활성색 amber), 보고서 3종 생성, 55000 외부접속</td>
</tr>
</tbody>
</table>
<p>상세 근거: <a href="docs/history/2026-06-22_POI투영오차-개선.md">docs/history/2026-06-22_POI투영오차-개선.md</a>(발명 B·C 1차 출처), <a href="docs/history/2026-06-26_1520_스테이션바-측점축-전환.md">2026-06-26_1520_측점축-전환</a>, <a href="docs/history/2026-06-29_1029_역사-통과-좌표근접-보강.md">2026-06-29_1029_역사-통과-좌표근접-보강</a> 등.</p>
<hr />
<h2 id="10-특허-관점-종합">10. 특허 관점 종합</h2>
<blockquote>
<p>출처: <a href="docs/특허_스테이션기반_주행영상_플레이어.html">docs/특허_스테이션기반_주행영상_플레이어.html</a>(기술발굴), <a href="docs/발명신고서_측점표고정합_역투영보정.html">docs/발명신고서_측점표고정합_역투영보정.html</a>(변리사 제출용).</p>
</blockquote>
<h3 id="101-기술요소-인벤토리-e1e9">10.1 기술요소 인벤토리 (E1~E9)</h3>
<table>
<thead>
<tr class="header">
<th>코드</th>
<th>요소</th>
<th>특허 적합성</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>E1</td>
<td>측점(체이니지) 기반 영상 색인·탐색</td>
<td>후보 A(좁게)</td>
</tr>
<tr class="even">
<td>E2</td>
<td>지리객체 실시간 영상 정합(labelMap 사전계산)</td>
<td>공지(큰 틀)</td>
</tr>
<tr class="odd">
<td>E3</td>
<td><strong>측점 표고 기반 DEM-free 지면고도 추정</strong></td>
<td><strong>후보 B(상)</strong></td>
</tr>
<tr class="even">
<td>E4</td>
<td><strong>역투영+슬랜트거리 보존 단일드래그 위치보정</strong></td>
<td><strong>후보 C(상)</strong></td>
</tr>
<tr class="odd">
<td>E5</td>
<td>비등방 거리필터+소실점 클러터 억제</td>
<td>후보 D(종속)</td>
</tr>
<tr class="even">
<td>E6</td>
<td>노이즈 강건 오버레이(이중평활·title짝·이상치거부)</td>
<td>후보 E(종속)</td>
</tr>
<tr class="odd">
<td>E7</td>
<td>측점 폴리라인→중심선</td>
<td>제외(통상 처리)</td>
</tr>
<tr class="even">
<td>E8</td>
<td>이종 CSV 융합(인코딩 자동감지)</td>
<td>제외</td>
</tr>
<tr class="odd">
<td>E9</td>
<td>비고 텍스트 방향전환 추출</td>
<td>제외</td>
</tr>
</tbody>
</table>
<h3 id="102-출원-권고-우선순위">10.2 출원 권고 우선순위</h3>
<ul>
<li><strong>1순위: 발명 B + C 결합</strong> — 신규성/진보성 우수, 글로벌+한국 신규성 확인.</li>
<li><strong>2순위: 후보 A(좁게)</strong> — LRS 비선형 재좌표화 + 회차간 위치앵커 + 다중회차 동기. 한국 선행기술(KR102268318B1·KR100411587B1) 위험 → 좁게 청구. <strong>본 명세의 측점축 노선좌표 고정(§6.3)·측점검색 순환(§6.6)·이중탐지 kmExists(§6.5)가 A의 구현 근거.</strong></li>
<li><strong>3순위: D·E 종속항 흡수</strong>. <strong>제외: E7·E8·E9</strong>.</li>
</ul>
<h3 id="103-청구-포인트-핵심-bc">10.3 청구 포인트 핵심 (B·C)</h3>
<ul>
<li><strong>발명 B 독립항</strong>: ①측점 실측 정표고 수열 → ②표고미상 대상에 최근접 측점 정표고를 지면고도 채택(노선기복 반영) → ③지오이드고 가산으로 타원체고 변환·datum 정합 → ④정합 투영. <strong>※ ②+③ 결합을 한 청구항에</strong>(개별요소는 공지).</li>
<li><strong>발명 C 독립항</strong>: ①드래그 개시 시 슬랜트거리 보존 → ②드롭픽셀 역투영 → ③단일 제스처로 수평·수직 동시 복원 → ④식별자 연계 영속. <strong>&quot;depth map 없이 거리보존&quot;</strong> 명시(US9188444B2 구별).</li>
</ul>
<h3 id="104-선행기술-조사-완료분">10.4 선행기술 (조사 완료분)</h3>
<ul>
<li>E1·E2 큰 틀은 공지: Esri ArcGIS Pro FMV, US9996976B2, Mandli Roadview, Remote GeoSystems LineVision, ENSCO Virtual Track Walk.</li>
<li>B·C는 글로벌+한국 신규성 확인. <strong>단, KIPRIS 전문(專門)검색 미수행 — 출원 전 필수.</strong></li>
</ul>
<h3 id="105-신규성-후보-종합-구현-기반">10.5 신규성 후보 종합 (구현 기반)</h3>
<ol type="1">
<li>드론 SRT pose 기반 <strong>실시간 GIS-영상 정합 파이프라인</strong>(타원체고/정표고 datum 자동 정합 포함).</li>
<li><strong>단일 대응점 sensorH 역산</strong> 인터랙티브 FOV 보정(가로 정합 보존, 중심부 발산 거부).</li>
<li><strong>깊이 모호성 분리형 역투영 3종</strong>(슬랜트유지/지면교차/표고 1차역산)을 보정 의도별 선택.</li>
<li><strong>에지(회전경계) 보존 적응형 평활</strong> + 속도적응 EMA(연속거부 한계).</li>
<li><strong>월드좌표 precompute + 연속포즈 재투영 2단 분리</strong>로 단일스레드 60fps AR.</li>
<li><strong>누적 이동거리/측점 비례 축 + 방향색 직교 분리</strong> 진행바, 노선좌표 고정(회차 정렬).</li>
<li><strong>좌표/측점 이중 탐지 + 합집합 보강 + kmExists 신뢰도 플래그</strong>(&quot;위치 표시&quot;&quot;측점 단정&quot; 분리).</li>
</ol>
<hr />
<h2 id="11-검증-결과표">11. 검증 결과표</h2>
<table>
<thead>
<tr class="header">
<th>항목</th>
<th>개선 전</th>
<th>개선 후</th>
<th>근거</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>부각 오차(측점 157K900, 91.3m)</td>
<td>42.7°</td>
<td><strong>11.0°</strong></td>
<td>§4.3</td>
</tr>
<tr class="even">
<td>지면고도 정확도(측점 vs 구글 DEM)</td>
<td></td>
<td><strong>0.2m(~6px@2160p)</strong></td>
<td>§4.3</td>
</tr>
<tr class="odd">
<td>역투영 왕복오차(z=42→py→z)</td>
<td></td>
<td><strong>≈0 (42.000)</strong></td>
<td>§7.2</td>
</tr>
<tr class="even">
<td>화면 EMA 지연</td>
<td>1.65s(α=0.01)</td>
<td>~25ms(α=0.4)</td>
<td>§5.3 / history 06-22</td>
</tr>
<tr class="odd">
<td>렌더</td>
<td>끊김</td>
<td><strong>60fps</strong>(precompute+RAF 분리)</td>
<td>§5.5</td>
</tr>
<tr class="even">
<td>역 재진입 마커</td>
<td>누락</td>
<td>좌표근접 합집합 보강으로 표출</td>
<td>§6.5</td>
</tr>
</tbody>
</table>
<p>지오이드고 N: 본 명세 <strong>25.8m</strong>(KNGeoid 검증) 통일. 초기 문서값 25.449m는 각주 처리.</p>
<hr />
<h2 id="12-후임자-인계">12. 후임자 인계</h2>
<h3 id="121-데이터-흐름-한눈에">12.1 데이터 흐름 한눈에</h3>
<p>geoStore(frames/stations/structures/routeMeta) → <code>useEffect</code> precompute(<code>viewedRef</code>=프레임별 km/chain/time, <code>chainRef</code>=time/frac/chain) → <code>pxAtTime</code>/<code>pxAtChain</code>·<code>barSegments</code>·<code>structureMarks</code> → Timeline 렌더. <strong>모든 px는 단일 매핑(<code>pxAtTime</code>/<code>pxAtChain</code>)을 통과</strong>, 역변환은 <code>timeAtFrac</code>/<code>chainAtTime</code>만 사용.</p>
<h3 id="122-튜닝-포인트">12.2 튜닝 포인트</h3>
<ul>
<li><strong>폴더 route.json</strong>: <code>stationTolerance</code>(통과/검색 허용오차, 20m), <code>direction</code>/<code>directionChanges</code>(방향 변형 필터), <code>endStationGapMeters</code>/<code>lengthKm</code>(미도착 폭), <code>startStationName</code>/<code>endStationName</code>.</li>
<li><strong>카메라(패널, 데이터셋별 저장)</strong>: <code>focalLen</code>, <code>sensorW</code>, <code>sensorH</code>(FOV), <code>yawOffset</code>/<code>pitch</code>/<code>roll</code>, <code>geoidOffset</code>(지역 N), <code>poiZOffset</code>.</li>
<li><strong>코드 상수</strong>: 평활 <code>W=8</code>, <code>YAW_EDGE=8°</code>, <code>HYST=10m</code>, 좌표통과 <code>TH=max(100,gmin·3)</code>, 역사 합집합 <code>24px</code>, 검색 중복 <code>0.3s</code>, EMA <code>β=0.25</code>/<code>REJECT_DIST=0.12</code>/<code>MAX_REJECT_FRAMES=8</code>.</li>
</ul>
<h3 id="123-빌드배포--필수-숙지">12.3 빌드/배포 (★ 필수 숙지)</h3>
<p>55000은 <strong>PM2 <code>ghiVideo</code><code>client/dist</code>(빌드 결과)를 정적 서빙</strong>한다. 소스만 고치면 화면에 반영 안 됨.</p>
<div class="sourceCode" id="cb17"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb17-1"><a href="#cb17-1" aria-hidden="true" tabindex="-1"></a><span class="bu">export</span> <span class="va">PATH=</span><span class="st">&quot;</span><span class="va">$HOME</span><span class="st">/.nvm/versions/node/v20.20.2/bin:</span><span class="va">$PATH</span><span class="st">&quot;</span> # 시스템 <span class="va">node</span> <span class="va">v12</span><span class="va">tsc</span>/vite 실패</span>
<span id="cb17-2"><a href="#cb17-2" aria-hidden="true" tabindex="-1"></a><span class="ex">npm</span> run build -w client # = tsc <span class="kw">&amp;&amp;</span> <span class="ex">vite</span> build → client/dist 갱신</span>
<span id="cb17-3"><a href="#cb17-3" aria-hidden="true" tabindex="-1"></a><span class="co"># 브라우저 Ctrl+Shift+R, 폴더 데이터는 런타임 로드라 폴더 재선택 필요</span></span></code></pre></div>
<p>외부(LAN) 접속: netsh portproxy(Windows :55000 → WSL IP:55000) + 방화벽. WSL IP는 재부팅 시 변동.</p>
<h3 id="124-업그레이드-포인트">12.4 업그레이드 포인트</h3>
<ol type="1">
<li>렌즈 왜곡(Brown-Conrady) 추가 → 가장자리 정합.</li>
<li>다점 대응 자동 캘리브레이션(focal·주점·왜곡 번들조정).</li>
<li>지오이드 격자(KNGeoid18) 보간으로 <code>geoidOffset</code> 자동화.</li>
<li>POI 표고 DEM 자동조회(현 부분지원).</li>
<li>중심선 per-frame 투영 공간색인, precompute Web Worker화.</li>
<li>frame.ts fps 고정(30)→<code>getVideoFps</code>, HLS jobs DB 큐, 멀티 Range 지원.</li>
<li><strong>코드 통합</strong>: 측점 파서/포매터(chainage.ts ↔︎ StationBar.tsx), 투영함수(<code>projectToChain</code> ↔︎ <code>projectChainage</code>) 단일화.</li>
</ol>
<hr />
<h2 id="13-한계--출원-전-확인사항">13. 한계 / 출원 전 확인사항</h2>
<ul>
<li><strong>KIPRIS 전문검색 미수행</strong> — 출원 전 필수.</li>
<li>지오코딩 수평오차는 데이터 한계(코드 보정은 드래그 수동).</li>
<li><code>geoidOffset</code> 지역 상수(대전 25.8m) — 타 지역 재튜닝.</li>
<li>VFR/abs_alt 노이즈, 먼 거리 역투영 민감.</li>
<li>미확정 항목(특허문서 <code>[확인 필요]</code> 유지): 다중회차 동기화 구현 범위, 위치앵커 주석 데이터모델, 권리귀속, 공개이력.</li>
<li>렌즈 왜곡 미보정(핀홀만) — 가장자리 정확도 트레이드오프.</li>
</ul>
<hr />
<p><em>본 문서는 5개 서브시스템(카메라 투영/자세 평활/측점·스테이션바/백엔드·데이터/기존 특허문서) 정밀 조사 결과를 종합한 것이다. 각 소스 참조는 클릭 가능한 파일:라인으로 추적 가능하며, 특허 출원 시 본 문서의 §3~§7·§10과 history 원문을 함께 변리사에게 제공할 것을 권장한다.</em></p>
</body>
</html>