GhiVideo 소스 복제 — v4 작업 시작 기준
기존 GhiVideo 저장소 HEAD의 트래킹 소스 362개 파일을 복제. (node_modules·storage·빌드 산출물·대용량 미디어는 .gitignore 규칙대로 제외) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,724 @@
|
||||
<!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>구현상세_드론좌표_영상투영_소스코드매칭</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">구현상세_드론좌표_영상투영_소스코드매칭</h1>
|
||||
</header>
|
||||
<h1 id="드론-좌표--영상-픽셀-투영-기술--소스코드-매칭-구현-상세">드론 좌표 → 영상 픽셀 투영: 기술 ↔︎ 소스코드 매칭 (구현 상세)</h1>
|
||||
<blockquote>
|
||||
<p>발표 자료 <a href="발표_핵심기술_드론좌표를_영상에_맞추기_쉬운설명.md">발표_핵심기술_드론좌표를_영상에_맞추기_쉬운설명</a> 의 "쉬운 비유"가 <strong>실제 어떤 코드로 구현</strong>되었는지, 파일·줄 위치와 함께 설명하는 개발자용 문서입니다. 핵심 계산은 대부분 <a href="../client/src/utils/geoProjection.ts">client/src/utils/geoProjection.ts</a> 한 곳에 모여 있고, 매 프레임 호출·평활·렌더링은 <a href="../client/src/components/overlay/StationOverlay.tsx">client/src/components/overlay/StationOverlay.tsx</a> 에 있습니다.</p>
|
||||
</blockquote>
|
||||
<style>
|
||||
figure.fig{margin:20px 0;text-align:center;page-break-inside:avoid;break-inside:avoid;}
|
||||
figure.fig svg{width:100%;max-width:700px;height:auto;border:1px solid #e7e0d2;border-radius:10px;background:#fffdf8;}
|
||||
figure.fig figcaption{font-size:0.9em;color:#777;margin-top:6px;}
|
||||
text{font-family:'Malgun Gothic','Hancom Gothic',sans-serif;}
|
||||
code{background:#f5f3ee;padding:1px 4px;border-radius:4px;}
|
||||
</style>
|
||||
|
||||
<hr />
|
||||
<h2 id="0-전체-파이프라인-파일-기준">0. 전체 파이프라인 (파일 기준)</h2>
|
||||
<figure class="fig">
|
||||
<svg viewBox="0 0 700 340" role="img" aria-label="전체 파이프라인">
|
||||
<defs><marker id="a" markerWidth="11" markerHeight="11" refX="7" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7 Z" fill="#7c3aed"></path></marker></defs>
|
||||
<rect x="200" y="12" width="300" height="40" rx="8" fill="#f1f5f9" stroke="#475569"></rect>
|
||||
<text x="350" y="30" text-anchor="middle" font-size="12" font-weight="bold" fill="#334155">데이터 로드 (폴더 선택)</text>
|
||||
<text x="350" y="45" text-anchor="middle" font-size="10" fill="#475569">geoData.ts → geoStore (드론 CSV·POI·측점·중심선)</text>
|
||||
<line x1="350" y1="52" x2="350" y2="70" stroke="#7c3aed" stroke-width="2" marker-end="url(#a)"></line>
|
||||
<rect x="150" y="72" width="400" height="34" rx="8" fill="#eef2ff" stroke="#4f46e5"></rect>
|
||||
<text x="350" y="94" text-anchor="middle" font-size="11" font-weight="bold" fill="#3730a3">매 프레임 루프: StationOverlay.tsx draw() @ requestAnimationFrame</text>
|
||||
<line x1="350" y1="106" x2="350" y2="124" stroke="#7c3aed" stroke-width="2" marker-end="url(#a)"></line>
|
||||
<g font-size="11">
|
||||
<rect x="120" y="126" width="460" height="30" rx="6" fill="#ecfdf5" stroke="#16a34a"></rect>
|
||||
<text x="350" y="146" text-anchor="middle" fill="#166534">① 각도→미터 latLonToTM / geoToEnu (proj4 · EPSG:5186)</text>
|
||||
<line x1="350" y1="156" x2="350" y2="170" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#a)"></line>
|
||||
<rect x="120" y="172" width="460" height="30" rx="6" fill="#fefce8" stroke="#ca8a04"></rect>
|
||||
<text x="350" y="192" text-anchor="middle" fill="#854d0e">② 드론기준 상대위치 buildRelEnu (빼기)</text>
|
||||
<line x1="350" y1="202" x2="350" y2="216" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#a)"></line>
|
||||
<rect x="120" y="218" width="460" height="30" rx="6" fill="#eff6ff" stroke="#2563eb"></rect>
|
||||
<text x="350" y="238" text-anchor="middle" fill="#1e3a8a">③ 방향 회전 buildRotation + applyRw2c (toCameraCoords)</text>
|
||||
<line x1="350" y1="248" x2="350" y2="262" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#a)"></line>
|
||||
<rect x="120" y="264" width="460" height="30" rx="6" fill="#fdf2f8" stroke="#db2777"></rect>
|
||||
<text x="350" y="284" text-anchor="middle" fill="#9d174d">④ 핀홀 투영 pixelFromCamera → (px,py)</text>
|
||||
<line x1="350" y1="294" x2="350" y2="308" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#a)"></line>
|
||||
<rect x="120" y="310" width="460" height="26" rx="6" fill="#f5f3ff" stroke="#7c3aed"></rect>
|
||||
<text x="350" y="327" text-anchor="middle" fill="#5b21b6">⑤⑥ 보간·평활(poseAt/smoothFrame/smoothStep) → ⑦ Canvas 렌더</text>
|
||||
</g>
|
||||
</svg>
|
||||
</figure>
|
||||
|
||||
<blockquote>
|
||||
<p>아래 표의 <strong>기술</strong>과 본문의 <strong>굵은 용어</strong>들은 문서 맨 아래 <strong><a href="#-용어-사전-도움말">📖 용어 사전(도움말)</a></strong> 에서 그림과 함께 자세히 풀어 놓았어요. 모르는 말이 나오면 바로 그 항목을 보면 됩니다. (다른 검색 필요 없음!)</p>
|
||||
</blockquote>
|
||||
<table>
|
||||
<thead>
|
||||
<tr class="header">
|
||||
<th>단계</th>
|
||||
<th>기술</th>
|
||||
<th>파일</th>
|
||||
<th>핵심 함수</th>
|
||||
<th><strong>하는 일 (쉽게)</strong></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="odd">
|
||||
<td>① 위경도→미터</td>
|
||||
<td>proj4 · EPSG:5186</td>
|
||||
<td>geoProjection.ts</td>
|
||||
<td><code>latLonToTM</code>, <code>geoToEnu</code></td>
|
||||
<td>GPS <strong>각도</strong>(위도·경도)를 계산하기 쉬운 <strong>미터 지도</strong>로 바꾼다</td>
|
||||
</tr>
|
||||
<tr class="even">
|
||||
<td>② 드론기준 위치</td>
|
||||
<td>ENU 상대좌표</td>
|
||||
<td>geoProjection.ts</td>
|
||||
<td><code>buildRelEnu</code></td>
|
||||
<td>터널−드론 좌표를 <strong>빼서</strong> "드론에서 동/북/위로 몇 m"인지 구한다</td>
|
||||
</tr>
|
||||
<tr class="odd">
|
||||
<td>③ 방향 회전</td>
|
||||
<td>회전행렬</td>
|
||||
<td>geoProjection.ts</td>
|
||||
<td><code>buildRotation</code>, <code>applyRw2c</code></td>
|
||||
<td>카메라 기울기(<strong>yaw·pitch·roll</strong>)만큼 방향을 <strong>한 번에 돌린다</strong></td>
|
||||
</tr>
|
||||
<tr class="even">
|
||||
<td>④ 화면 투영</td>
|
||||
<td>핀홀 카메라 모델</td>
|
||||
<td>geoProjection.ts</td>
|
||||
<td><code>pixelFromCamera</code></td>
|
||||
<td>3D 방향을 <strong>납작한 화면의 점(가로·세로 %)</strong> 으로 눌러 담는다</td>
|
||||
</tr>
|
||||
<tr class="odd">
|
||||
<td>⑤ 사이 채우기</td>
|
||||
<td>보간</td>
|
||||
<td>StationOverlay.tsx</td>
|
||||
<td><code>poseAt</code></td>
|
||||
<td>사진 30장 <strong>사이의 중간 위치</strong>를 상상해 채워 부드럽게</td>
|
||||
</tr>
|
||||
<tr class="even">
|
||||
<td>⑥ 흔들림 제거</td>
|
||||
<td>이동평균 + EMA</td>
|
||||
<td>StationOverlay.tsx</td>
|
||||
<td><code>smoothFrame</code>, <code>smoothStep</code></td>
|
||||
<td>여러 값을 <strong>평균 내</strong> 드론 떨림·튐을 없앤다</td>
|
||||
</tr>
|
||||
<tr class="odd">
|
||||
<td>⑦ 렌더</td>
|
||||
<td>RAF + Canvas 2D</td>
|
||||
<td>StationOverlay.tsx</td>
|
||||
<td><code>draw</code></td>
|
||||
<td>화면 새로 그릴 때마다(≈60/s) <strong>이름표를 캔버스에 그린다</strong></td>
|
||||
</tr>
|
||||
<tr class="even">
|
||||
<td>⑧ fps 자동</td>
|
||||
<td>프레임수÷길이</td>
|
||||
<td>VideoPlayer.tsx</td>
|
||||
<td><code>effectiveFps</code></td>
|
||||
<td>영상이 <strong>1초에 몇 장</strong>인지 스스로 알아내 ①~⑦의 시간 기준을 맞춘다</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h2 id="1-위경도각도--미터-지도-proj4--epsg5186">1. 위경도(각도) → 미터 지도 (proj4 · EPSG:5186)</h2>
|
||||
<p><strong>위치:</strong> <a href="../client/src/utils/geoProjection.ts#L74-L97">geoProjection.ts:74-97</a></p>
|
||||
<div class="sourceCode" id="cb1"><pre class="sourceCode ts"><code class="sourceCode typescript"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a>proj4<span class="op">.</span><span class="fu">defs</span>(<span class="st">'EPSG:5186'</span><span class="op">,</span></span>
|
||||
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a> <span class="st">'+proj=tmerc +lat_0=38 +lon_0=127 +k=1 +x_0=200000 +y_0=600000 +ellps=GRS80 +units=m +no_defs'</span>)<span class="op">;</span></span>
|
||||
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a>const _toTM <span class="op">=</span> <span class="fu">proj4</span>(<span class="st">'EPSG:4326'</span><span class="op">,</span> <span class="st">'EPSG:5186'</span>)<span class="op">;</span></span>
|
||||
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">latLonToTM</span>(lat<span class="op">,</span> lon) { <span class="co">// 위경도(각도) → TM(미터)</span></span>
|
||||
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a> 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> <span class="co">// 주의: proj4 는 [lon, lat] 순서</span></span>
|
||||
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a> return [e<span class="op">,</span> n]<span class="op">;</span></span>
|
||||
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a>}</span></code></pre></div>
|
||||
<ul>
|
||||
<li><strong>무엇:</strong> WGS84 위경도(<code>EPSG:4326</code>) → 한국 TM(<code>EPSG:5186</code>, 중부원점) 미터 좌표로 변환.</li>
|
||||
<li><strong>왜:</strong> 각도로는 거리를 못 재므로, 이후 모든 계산을 미터로 하기 위한 출발점.</li>
|
||||
<li><strong>역방향:</strong> <code>_toTM.inverse([E,N])</code> → 다시 위경도 (드래그 보정 <code>groundPointFromPixel</code> 등에서 사용).</li>
|
||||
</ul>
|
||||
<hr />
|
||||
<h2 id="2-드론-기준-상대-위치-enu-좌표">2. 드론 기준 상대 위치 (ENU 좌표)</h2>
|
||||
<p><strong>위치:</strong> <a href="../client/src/utils/geoProjection.ts#L255-L273">geoProjection.ts:255-273 <code>buildRelEnu</code></a></p>
|
||||
<div class="sourceCode" id="cb2"><pre class="sourceCode ts"><code class="sourceCode typescript"><span id="cb2-1"><a href="#cb2-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> geoidOffset<span class="op">,</span> <span class="op">...</span>)<span class="op">;</span> <span class="co">// 터널(대상)</span></span>
|
||||
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a>const drEnu <span class="op">=</span> <span class="fu">geoToEnu</span>(camera<span class="op">.</span><span class="at">lat</span><span class="op">,</span> camera<span class="op">.</span><span class="at">lon</span><span class="op">,</span> camera<span class="op">.</span><span class="at">altitude</span><span class="op">,</span> <span class="op">...</span>)<span class="op">;</span> <span class="co">// 드론(카메라)</span></span>
|
||||
<span id="cb2-3"><a href="#cb2-3" 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 class="co">// 위치 미세보정</span></span>
|
||||
<span id="cb2-4"><a href="#cb2-4" 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 class="co">// 빼기</span></span>
|
||||
<span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a>const dist <span class="op">=</span> <span class="bu">Math</span><span class="op">.</span><span class="fu">hypot</span>(relEnu[<span class="dv">0</span>]<span class="op">,</span> relEnu[<span class="dv">1</span>])<span class="op">;</span> <span class="co">// 수평거리(m)</span></span></code></pre></div>
|
||||
<ul>
|
||||
<li><strong>무엇:</strong> 대상과 드론을 미터 좌표로 놓고 빼서 "드론 기준 동/북/상 몇 m"(상대 벡터) 산출.</li>
|
||||
<li><strong>왜:</strong> 카메라에서 본 방향 계산의 입력. (<code>geoToEnu</code>는 <a href="../client/src/utils/geoProjection.ts#L91-L97">geoProjection.ts:91-97</a>)</li>
|
||||
<li><strong><code>geoidOffset</code>:</strong> 대상 정표고(EL)+지오이드고 → 타원체고. 드론 GPS 고도(타원체고)와 기준을 맞춤(대전≈25.8m).</li>
|
||||
</ul>
|
||||
<hr />
|
||||
<h2 id="3-카메라-기울기--회전행렬-rotation-matrix">3. 카메라 기울기 = 회전행렬 (Rotation Matrix)</h2>
|
||||
<p><strong>위치:</strong> <a href="../client/src/utils/geoProjection.ts#L275-L295">geoProjection.ts:275-295</a> (<code>buildRotation</code> + <code>applyRw2c</code>)</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><span class="kw">function</span> <span class="fu">buildRotation</span>(camera<span class="op">,</span> params) { <span class="co">// yaw·pitch·roll → 3×3 행렬</span></span>
|
||||
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a> const yaw <span class="op">=</span> <span class="fu">toRad</span>(camera<span class="op">.</span><span class="at">yaw</span> <span class="op">+</span> params<span class="op">.</span><span class="at">yawOffset</span>)<span class="op">;</span></span>
|
||||
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a> const pitch <span class="op">=</span> <span class="fu">toRad</span>(camera<span class="op">.</span><span class="at">pitch</span> <span class="op">+</span> params<span class="op">.</span><span class="at">pitch</span>)<span class="op">;</span></span>
|
||||
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a> const roll <span class="op">=</span> <span class="fu">toRad</span>(camera<span class="op">.</span><span class="at">roll</span> <span class="op">+</span> params<span class="op">.</span><span class="at">roll</span>)<span class="op">;</span></span>
|
||||
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a> const cy<span class="op">=</span><span class="fu">cos</span>(yaw)<span class="op">,</span> sy<span class="op">=</span><span class="fu">sin</span>(yaw)<span class="op">,</span> cp<span class="op">=</span><span class="fu">cos</span>(pitch)<span class="op">,</span> sp<span class="op">=</span><span class="fu">sin</span>(pitch)<span class="op">,</span> cr<span class="op">=</span><span class="fu">cos</span>(roll)<span class="op">,</span> sr<span class="op">=</span><span class="fu">sin</span>(roll)<span class="op">;</span></span>
|
||||
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a> return [[cy<span class="op">*</span>cr<span class="op">+</span>sy<span class="op">*</span>sp<span class="op">*</span>sr<span class="op">,</span> sy<span class="op">*</span>cp<span class="op">,</span> cy<span class="op">*</span>sr<span class="op">-</span>sy<span class="op">*</span>sp<span class="op">*</span>cr]<span class="op">,</span> <span class="op">...</span>]<span class="op">;</span> <span class="co">// R_b2w</span></span>
|
||||
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a>}</span>
|
||||
<span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">applyRw2c</span>(b2w<span class="op">,</span> rel) { <span class="co">// R_w2c · rel → 카메라 좌표</span></span>
|
||||
<span id="cb3-9"><a href="#cb3-9" aria-hidden="true" tabindex="-1"></a> return { 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>
|
||||
<span id="cb3-10"><a href="#cb3-10" 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>)<span class="op">,</span> 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> }<span class="op">;</span></span>
|
||||
<span id="cb3-11"><a href="#cb3-11" aria-hidden="true" tabindex="-1"></a>}</span></code></pre></div>
|
||||
<ul>
|
||||
<li><strong>무엇:</strong> yaw(좌우)·pitch(상하)·roll(갸웃)을 하나의 3×3 행렬로 만들어, 상대벡터를 <strong>카메라 시점 좌표 <code>(Xc,Yc,Zc)</code></strong> 로 한 번에 회전.</li>
|
||||
<li><strong>왜:</strong> 세 회전을 개별 적용하지 않고 행렬 한 번 곱으로 정확·간결하게. (문서의 "만능 양념장")</li>
|
||||
<li><strong>원본과 동일:</strong> <code>R_w2c = R_align · R_b2wᵀ</code> (파이썬 <code>advanced_tuner_v2.py</code> 이식 — 파일 상단 주석 <a href="../client/src/utils/geoProjection.ts#L1-L12">geoProjection.ts:1-12</a>).</li>
|
||||
<li><strong>묶음 함수:</strong> ②+③을 한 번에 = <a href="../client/src/utils/geoProjection.ts#L301-L317"><code>toCameraCoords</code>(301-317)</a>. <code>distH/fwd/side</code>(거리필터용)도 여기서 채움.</li>
|
||||
</ul>
|
||||
<hr />
|
||||
<h2 id="4-바늘구멍-사진기--핀홀-투영-초점거리">4. 바늘구멍 사진기 = 핀홀 투영 (초점거리)</h2>
|
||||
<p><strong>위치:</strong> <a href="../client/src/utils/geoProjection.ts#L126-L137">geoProjection.ts:126-137 <code>pixelFromCamera</code></a></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>export <span class="kw">function</span> <span class="fu">pixelFromCamera</span>(cc<span class="op">,</span> params) {</span>
|
||||
<span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a> const f <span class="op">=</span> params<span class="op">.</span><span class="at">focalLen</span><span class="op">,</span> sW <span class="op">=</span> params<span class="op">.</span><span class="at">sensorW</span> <span class="op">??</span> <span class="dv">36</span><span class="op">,</span> sH <span class="op">=</span> params<span class="op">.</span><span class="at">sensorH</span> <span class="op">??</span> <span class="fl">20.25</span><span class="op">;</span></span>
|
||||
<span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a> return {</span>
|
||||
<span id="cb4-4"><a href="#cb4-4" aria-hidden="true" tabindex="-1"></a> pxRaw<span class="op">:</span> (<span class="fl">0.5</span> <span class="op">+</span> params<span class="op">.</span><span class="at">cx0</span>) <span class="op">+</span> (cc<span class="op">.</span><span class="at">Xc</span> <span class="op">/</span> cc<span class="op">.</span><span class="at">Zc</span>) <span class="op">*</span> (f <span class="op">/</span> sW)<span class="op">,</span> <span class="co">// 가로 0~1</span></span>
|
||||
<span id="cb4-5"><a href="#cb4-5" aria-hidden="true" tabindex="-1"></a> pyRaw<span class="op">:</span> (<span class="fl">0.5</span> <span class="op">+</span> params<span class="op">.</span><span class="at">cy0</span>) <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> (f <span class="op">/</span> sH)<span class="op">,</span> <span class="co">// 세로 0~1</span></span>
|
||||
<span id="cb4-6"><a href="#cb4-6" aria-hidden="true" tabindex="-1"></a> }<span class="op">;</span></span>
|
||||
<span id="cb4-7"><a href="#cb4-7" aria-hidden="true" tabindex="-1"></a>}</span></code></pre></div>
|
||||
<ul>
|
||||
<li><strong>무엇:</strong> 카메라 좌표 → 화면 정규좌표(0~1). 문서의 "창문 스티커" 계산이 이 두 줄.</li>
|
||||
<li><strong>원근:</strong> <code>Xc/Zc</code>, <code>Yc/Zc</code> — 앞쪽 거리 <code>Zc</code>로 나누므로 <strong>멀수록 가운데로 작게</strong>.</li>
|
||||
<li><strong>초점거리:</strong> <code>f/sW</code>, <code>f/sH</code> — <code>f</code>(focalLen)가 클수록(망원) 크게. <code>sensorH=20.25</code>는 16:9 기준(=36×9/16).</li>
|
||||
<li><strong>참고:</strong> 원스톱 함수 <a href="../client/src/utils/geoProjection.ts#L334-L427"><code>projectPoint</code>(334-427)</a>는 ①~④+FOV판정까지 한 번에 수행(디버그/단건용). 실사용 렌더는 <code>toCameraCoords</code>+<code>pixelFromCamera</code>를 프레임마다 호출.</li>
|
||||
</ul>
|
||||
<hr />
|
||||
<h2 id="5-30장-사이-부드럽게--보간-interpolation">5. 30장 사이 부드럽게 = 보간 (Interpolation)</h2>
|
||||
<p><strong>위치:</strong> <a href="../client/src/components/overlay/StationOverlay.tsx#L689">StationOverlay.tsx:689-712 <code>poseAt</code></a></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>const poseAt <span class="op">=</span> (estFrame) <span class="kw">=></span> { <span class="co">// 연속 프레임번호 → 드론 포즈</span></span>
|
||||
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a> const frac <span class="op">=</span> (estFrame <span class="op">-</span> f1) <span class="op">/</span> (f2 <span class="op">-</span> f1)<span class="op">;</span> <span class="co">// 두 실제 프레임 사이 위치(0~1)</span></span>
|
||||
<span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a> const L <span class="op">=</span> (x<span class="op">,</span> y) <span class="kw">=></span> x <span class="op">+</span> (y <span class="op">-</span> x) <span class="op">*</span> frac<span class="op">;</span> <span class="co">// 선형보간</span></span>
|
||||
<span id="cb5-4"><a href="#cb5-4" 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> <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="cb5-5"><a href="#cb5-5" aria-hidden="true" tabindex="-1"></a> return { <span class="op">...</span>a<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> lon<span class="op">:</span> <span class="fu">L</span>(a<span class="op">.</span><span class="at">lon</span><span class="op">,</span>b<span class="op">.</span><span class="at">lon</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 class="op">...</span> }<span class="op">;</span></span>
|
||||
<span id="cb5-6"><a href="#cb5-6" aria-hidden="true" tabindex="-1"></a>}<span class="op">;</span></span></code></pre></div>
|
||||
<ul>
|
||||
<li><strong>무엇:</strong> 정수 프레임(사진 30장) 사이의 <strong>중간 드론 위치·자세</strong>를 계산.</li>
|
||||
<li><strong>왜:</strong> 이름표가 30번이 아니라 화면 갱신(≈60번)마다 매끈하게 이동.</li>
|
||||
<li><strong>입력:</strong> 현재 시각 → 연속 프레임번호 <code>estFrame = estTime * fpsRef.current</code> (<a href="../client/src/components/overlay/StationOverlay.tsx#L974">StationOverlay.tsx:974</a>). <code>fpsRef</code>는 §8에서 주입.</li>
|
||||
</ul>
|
||||
<hr />
|
||||
<h2 id="6-흔들림-제거--이동평균--ema-평활">6. 흔들림 제거 = 이동평균 + EMA 평활</h2>
|
||||
<p><strong>위치 A — 원본 데이터 평균:</strong> <a href="../client/src/components/overlay/StationOverlay.tsx#L596">StationOverlay.tsx:596-620 <code>smoothFrame</code></a></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><span class="co">// 중심 프레임 기준 ±halfWin 프레임의 GPS·자세를 평균 (회전 경계는 보존)</span></span>
|
||||
<span id="cb6-2"><a href="#cb6-2" aria-hidden="true" tabindex="-1"></a>lat <span class="op">=</span> Σlat<span class="op">/</span>n<span class="op">;</span> yaw <span class="op">=</span> <span class="fu">atan2</span>(Σsin<span class="op">,</span> Σcos)<span class="op">;</span> <span class="co">// 각도는 sin/cos 평균 후 atan2</span></span></code></pre></div>
|
||||
<p><strong>위치 B — 화면 위치 평활:</strong> <a href="../client/src/components/overlay/StationOverlay.tsx#L48">StationOverlay.tsx:48-61 <code>smoothStep</code></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><span class="fu">if</span> (d <span class="op">></span> REJECT_DIST <span class="op">&&</span> prev<span class="op">.</span><span class="at">rej</span> <span class="op"><</span> MAX) return {유지}<span class="op">;</span> <span class="co">// 튀는 값(이상치) 무시</span></span>
|
||||
<span id="cb7-2"><a href="#cb7-2" aria-hidden="true" tabindex="-1"></a>const speed <span class="op">=</span> <span class="fu">hypot</span>(vx<span class="op">,</span> vy)<span class="op">;</span> <span class="co">// 평활된 이동속도</span></span>
|
||||
<span id="cb7-3"><a href="#cb7-3" aria-hidden="true" tabindex="-1"></a>const a <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="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="cb7-4"><a href="#cb7-4" 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 class="co">// 속도적응 EMA</span></span></code></pre></div>
|
||||
<ul>
|
||||
<li><strong>무엇:</strong> (A) GPS·자세 노이즈를 프레임 평균으로 줄이고, (B) 화면 좌표를 속도적응 EMA로 부드럽게 + 이상치 거부.</li>
|
||||
<li><strong>왜:</strong> 드론 떨림에도 이름표가 안정적으로 붙어 있게. (느릴 땐 강하게 평활, 빠를 땐 즉시 추종)</li>
|
||||
</ul>
|
||||
<hr />
|
||||
<h2 id="7-매-프레임-렌더--requestanimationframe--canvas-2d">7. 매 프레임 렌더 = requestAnimationFrame + Canvas 2D</h2>
|
||||
<p><strong>위치:</strong> <a href="../client/src/components/overlay/StationOverlay.tsx#L931">StationOverlay.tsx:931-1243 <code>draw</code> 루프</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 draw <span class="op">=</span> () <span class="kw">=></span> {</span>
|
||||
<span id="cb8-2"><a href="#cb8-2" aria-hidden="true" tabindex="-1"></a> rafId <span class="op">=</span> <span class="fu">requestAnimationFrame</span>(draw)<span class="op">;</span> <span class="co">// 화면 갱신마다 반복(≈60/s)</span></span>
|
||||
<span id="cb8-3"><a href="#cb8-3" aria-hidden="true" tabindex="-1"></a> const dronePose <span class="op">=</span> <span class="fu">poseAt</span>(estFrame)<span class="op">;</span> <span class="co">// §5 보간 포즈</span></span>
|
||||
<span id="cb8-4"><a href="#cb8-4" aria-hidden="true" tabindex="-1"></a> <span class="co">// POI 마다:</span></span>
|
||||
<span id="cb8-5"><a href="#cb8-5" aria-hidden="true" tabindex="-1"></a> const cc <span class="op">=</span> <span class="fu">toCameraCoords</span>(dronePose<span class="op">,</span> poiA<span class="op">.</span><span class="at">lat</span><span class="op">,</span> poiA<span class="op">.</span><span class="at">lon</span><span class="op">,</span> pz<span class="op">,</span> params<span class="op">,</span> worldOrigin)<span class="op">;</span> <span class="co">// §2+§3</span></span>
|
||||
<span id="cb8-6"><a href="#cb8-6" aria-hidden="true" tabindex="-1"></a> const { pxRaw<span class="op">,</span> pyRaw } <span class="op">=</span> <span class="fu">pixelFromCamera</span>(cc<span class="op">,</span> params)<span class="op">;</span> <span class="co">// §4</span></span>
|
||||
<span id="cb8-7"><a href="#cb8-7" aria-hidden="true" tabindex="-1"></a> const d <span class="op">=</span> <span class="fu">smoothStep</span>(prevDpoi<span class="op">,</span> pxRaw<span class="op">,</span> pyRaw<span class="op">,</span> <span class="op">...</span>)<span class="op">;</span> <span class="co">// §6 화면 평활</span></span>
|
||||
<span id="cb8-8"><a href="#cb8-8" aria-hidden="true" tabindex="-1"></a> ctx<span class="op">.</span><span class="fu">strokeText</span>(label<span class="op">,</span> lx<span class="op">,</span> labelY)<span class="op">;</span> ctx<span class="op">.</span><span class="fu">fillText</span>(<span class="op">...</span>)<span class="op">;</span> <span class="co">// Canvas 에 이름표 그림</span></span>
|
||||
<span id="cb8-9"><a href="#cb8-9" aria-hidden="true" tabindex="-1"></a>}<span class="op">;</span></span></code></pre></div>
|
||||
<ul>
|
||||
<li><strong>무엇:</strong> 화면이 새로 그려질 때마다 §2~§6을 다시 계산해 <strong>Canvas 2D</strong>에 이름표·중심선을 그림.</li>
|
||||
<li><strong>호출 지점:</strong> 측점 라벨 <a href="../client/src/components/overlay/StationOverlay.tsx#L1070-L1072">1070-1072</a>, POI/구조물 라벨 <a href="../client/src/components/overlay/StationOverlay.tsx#L1107-L1109">1107-1109</a>.</li>
|
||||
<li><strong>성능:</strong> 무거운 "가시집합/겹침 판정"은 별도 사전계산(<code>requestIdleCallback</code>, 파일 상단 주석), RAF는 투영·그리기 위주.</li>
|
||||
</ul>
|
||||
<hr />
|
||||
<h2 id="8-영상별-fps-자동-산출">8. 영상별 fps 자동 산출</h2>
|
||||
<p><strong>위치:</strong> <a href="../client/src/components/player/VideoPlayer.tsx#L272">VideoPlayer.tsx:272-286 <code>effectiveFps</code></a></p>
|
||||
<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 effectiveFps <span class="op">=</span> <span class="fu">useMemo</span>(() <span class="kw">=></span> {</span>
|
||||
<span id="cb9-2"><a href="#cb9-2" aria-hidden="true" tabindex="-1"></a> <span class="fu">if</span> (<span class="op">!</span>storeFrames<span class="op">.</span><span class="at">length</span> <span class="op">||</span> <span class="op">!</span>duration) return <span class="dv">30000</span><span class="op">/</span><span class="dv">1001</span><span class="op">;</span> <span class="co">// 폴백 29.97</span></span>
|
||||
<span id="cb9-3"><a href="#cb9-3" aria-hidden="true" tabindex="-1"></a> let maxF <span class="op">=</span> <span class="dv">0</span><span class="op">;</span> <span class="fu">for</span> (const f of storeFrames) <span class="fu">if</span> (f<span class="op">.</span><span class="at">frame</span> <span class="op">></span> maxF) maxF <span class="op">=</span> f<span class="op">.</span><span class="at">frame</span><span class="op">;</span></span>
|
||||
<span id="cb9-4"><a href="#cb9-4" aria-hidden="true" tabindex="-1"></a> const raw <span class="op">=</span> maxF <span class="op">/</span> duration<span class="op">;</span> <span class="co">// 마지막 프레임번호 ÷ 영상길이</span></span>
|
||||
<span id="cb9-5"><a href="#cb9-5" aria-hidden="true" tabindex="-1"></a> const STD <span class="op">=</span> [<span class="fl">23.976</span><span class="op">,</span><span class="dv">24</span><span class="op">,</span><span class="dv">25</span><span class="op">,</span><span class="fl">29.97</span><span class="op">,</span><span class="dv">30</span><span class="op">,</span><span class="dv">50</span><span class="op">,</span><span class="fl">59.94</span><span class="op">,</span><span class="dv">60</span>]<span class="op">;</span></span>
|
||||
<span id="cb9-6"><a href="#cb9-6" aria-hidden="true" tabindex="-1"></a> let best <span class="op">=</span> STD[<span class="dv">0</span>]<span class="op">,</span> bd <span class="op">=</span> <span class="bu">Math</span><span class="op">.</span><span class="fu">abs</span>(raw<span class="op">-</span>STD[<span class="dv">0</span>])<span class="op">;</span></span>
|
||||
<span id="cb9-7"><a href="#cb9-7" aria-hidden="true" tabindex="-1"></a> <span class="fu">for</span> (const s of STD) { const d <span class="op">=</span> <span class="bu">Math</span><span class="op">.</span><span class="fu">abs</span>(raw<span class="op">-</span>s)<span class="op">;</span> <span class="fu">if</span> (d<span class="op"><</span>bd){bd<span class="op">=</span>d<span class="op">;</span>best<span class="op">=</span>s<span class="op">;</span>} }</span>
|
||||
<span id="cb9-8"><a href="#cb9-8" aria-hidden="true" tabindex="-1"></a> return bd <span class="op"><=</span> best<span class="op">*</span><span class="fl">0.1</span> <span class="op">?</span> best <span class="op">:</span> raw<span class="op">;</span> <span class="co">// 표준값 ±10%면 스냅</span></span>
|
||||
<span id="cb9-9"><a href="#cb9-9" aria-hidden="true" tabindex="-1"></a>}<span class="op">,</span> [storeFrames<span class="op">,</span> duration])<span class="op">;</span></span></code></pre></div>
|
||||
<ul>
|
||||
<li><strong>무엇:</strong> 드론 CSV에는 시간이 없고 프레임 번호(<code>frame_cnt</code>)만 있으므로, <strong>영상 길이</strong>와 결합해 fps 산출 후 표준값에 스냅.</li>
|
||||
<li><strong>연결:</strong> <code><StationOverlay fps={effectiveFps} /></code> → <a href="../client/src/components/overlay/StationOverlay.tsx#L260">StationOverlay.tsx:260 <code>fpsRef</code></a> → §5의 <code>estFrame</code>, 최근접 프레임 탐색의 기준.</li>
|
||||
<li><strong>전제:</strong> 드론 CSV가 영상 전 구간을 덮는다고 가정(마지막 frame_cnt ≈ 영상 끝). 부분만 덮으면 스냅 실패 시 원시값 사용.</li>
|
||||
</ul>
|
||||
<hr />
|
||||
<h2 id="부록-데이터-로딩-폴더--스토어">부록. 데이터 로딩 (폴더 → 스토어)</h2>
|
||||
<p><strong>위치:</strong> <a href="../client/src/utils/geoData.ts">geoData.ts</a> → <a href="../client/src/store/geoStore.ts">geoStore.ts</a></p>
|
||||
<ul>
|
||||
<li><code><input webkitdirectory></code> 로 고른 폴더의 <code><base>.csv</code>(드론), <code>_POI.csv</code>, <code>building/*.csv</code>(측점·교량·터널·구교)를 파싱.</li>
|
||||
<li>드론 CSV 헤더: <code>frame_cnt,latitude,longitude,altitude,yaw,pitch,roll,focal_len</code> (<a href="../client/src/utils/geoData.ts#L139">geoData.ts:139-172</a>).</li>
|
||||
<li>인코딩 자동감지(UTF-8 BOM / EUC-KR), KMZ(zip)는 <code>fflate</code> 로 해제.</li>
|
||||
<li>ENU 기준원점: <code>getWorldOrigin</code> (<a href="../client/src/utils/geoData.ts#L576">geoData.ts:576</a>) → 스토어 <code>origin</code> → 투영에 <code>ref</code> 로 전달.</li>
|
||||
</ul>
|
||||
<hr />
|
||||
<h2 id="관련-함수-빠른-색인">관련 함수 빠른 색인</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr class="header">
|
||||
<th>함수</th>
|
||||
<th>파일:줄</th>
|
||||
<th>역할</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="odd">
|
||||
<td><code>latLonToTM</code> / <code>geoToEnu</code></td>
|
||||
<td>geoProjection.ts:81 / 91</td>
|
||||
<td>각도→미터, ENU</td>
|
||||
</tr>
|
||||
<tr class="even">
|
||||
<td><code>buildRelEnu</code></td>
|
||||
<td>geoProjection.ts:255</td>
|
||||
<td>드론기준 상대벡터</td>
|
||||
</tr>
|
||||
<tr class="odd">
|
||||
<td><code>buildRotation</code> / <code>applyRw2c</code></td>
|
||||
<td>geoProjection.ts:275 / 289</td>
|
||||
<td>회전행렬·적용</td>
|
||||
</tr>
|
||||
<tr class="even">
|
||||
<td><code>toCameraCoords</code></td>
|
||||
<td>geoProjection.ts:301</td>
|
||||
<td>②+③ 묶음</td>
|
||||
</tr>
|
||||
<tr class="odd">
|
||||
<td><code>pixelFromCamera</code></td>
|
||||
<td>geoProjection.ts:126</td>
|
||||
<td>핀홀 투영</td>
|
||||
</tr>
|
||||
<tr class="even">
|
||||
<td><code>projectPoint</code></td>
|
||||
<td>geoProjection.ts:334</td>
|
||||
<td>①~④ 원스톱(디버그)</td>
|
||||
</tr>
|
||||
<tr class="odd">
|
||||
<td><code>worldFromPixel</code> / <code>groundPointFromPixel</code> / <code>solveZForPixelY</code></td>
|
||||
<td>geoProjection.ts:149 / 218 / 194</td>
|
||||
<td>역투영(드래그 보정)</td>
|
||||
</tr>
|
||||
<tr class="even">
|
||||
<td><code>poseAt</code> / <code>smoothFrame</code> / <code>smoothStep</code></td>
|
||||
<td>StationOverlay.tsx:689 / 596 / 48</td>
|
||||
<td>보간·평활</td>
|
||||
</tr>
|
||||
<tr class="odd">
|
||||
<td><code>draw</code></td>
|
||||
<td>StationOverlay.tsx:931</td>
|
||||
<td>RAF 렌더 루프</td>
|
||||
</tr>
|
||||
<tr class="even">
|
||||
<td><code>effectiveFps</code></td>
|
||||
<td>VideoPlayer.tsx:272</td>
|
||||
<td>fps 자동 산출</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h1 id="-용어-사전-도움말">📖 용어 사전 (도움말)</h1>
|
||||
<blockquote>
|
||||
<p>본문에 나온 전문용어를 <strong>초등학생도 이해할 수 있게</strong> 그림과 함께 풀었어요. 모르는 말이 나오면 여기만 보면 돼요. (다른 검색 필요 없음!)</p>
|
||||
</blockquote>
|
||||
<h2 id="-위도경도-latitude--longitude">▸ 위도·경도 (latitude / longitude)</h2>
|
||||
<p>지구 위의 위치를 나타내는 <strong>두 개의 각도</strong>예요. <strong>지구 중심에서 각도기로 잰 각(도, °)</strong> 이에요.</p>
|
||||
<ul>
|
||||
<li><strong>위도</strong> = 적도(0도)에서 <strong>위/아래로</strong> 몇 도 (북극 90도, 남극 −90도)</li>
|
||||
<li><strong>경도</strong> = 영국(0도)에서 <strong>옆으로 빙 둘러</strong> 몇 도 (동쪽으로 갈수록 커짐)</li>
|
||||
</ul>
|
||||
<figure class="fig">
|
||||
<svg viewBox="0 0 700 220" role="img" aria-label="위도와 경도는 지구 중심에서 잰 각도">
|
||||
<!-- 위도 -->
|
||||
<g>
|
||||
<circle cx="160" cy="110" r="80" fill="#eef6ff" stroke="#2563eb"></circle>
|
||||
<line x1="80" y1="110" x2="240" y2="110" stroke="#93c5fd"></line>
|
||||
<text x="250" y="113" font-size="10" fill="#1e3a8a">적도 0°</text>
|
||||
<line x1="160" y1="110" x2="215" y2="52" stroke="#ef4444" stroke-width="2"></line>
|
||||
<path d="M200,110 A55,55 0 0 0 190,75" fill="none" stroke="#ef4444"></path>
|
||||
<text x="205" y="95" font-size="11" fill="#b91c1c" font-weight="bold">위도</text>
|
||||
<circle cx="160" cy="110" r="3" fill="#111"></circle>
|
||||
<text x="160" y="205" text-anchor="middle" font-size="11" fill="#1e3a8a">위도 = 위아래 각도 (중심에서 잼)</text>
|
||||
</g>
|
||||
<!-- 경도 -->
|
||||
<g>
|
||||
<circle cx="500" cy="110" r="80" fill="#f0fdf4" stroke="#16a34a"></circle>
|
||||
<ellipse cx="500" cy="110" rx="30" ry="80" fill="none" stroke="#86efac"></ellipse>
|
||||
<ellipse cx="500" cy="110" rx="60" ry="80" fill="none" stroke="#86efac"></ellipse>
|
||||
<line x1="500" y1="110" x2="500" y2="30" stroke="#16a34a"></line>
|
||||
<text x="500" y="26" text-anchor="middle" font-size="10" fill="#166534">0° (영국)</text>
|
||||
<line x1="500" y1="110" x2="565" y2="65" stroke="#ea580c" stroke-width="2"></line>
|
||||
<path d="M500,50 A60,60 0 0 1 548,72" fill="none" stroke="#ea580c"></path>
|
||||
<text x="530" y="52" font-size="11" fill="#9a3412" font-weight="bold">경도</text>
|
||||
<circle cx="500" cy="110" r="3" fill="#111"></circle>
|
||||
<text x="500" y="205" text-anchor="middle" font-size="11" fill="#166534">경도 = 옆으로 도는 각도</text>
|
||||
</g>
|
||||
</svg>
|
||||
<figcaption>둘 다 지구 '중심'에서 잰 각도(도). 그래서 단위가 미터가 아니라 '도(°)'다.</figcaption>
|
||||
</figure>
|
||||
|
||||
<h2 id="-wgs84--epsg4326">▸ WGS84 · EPSG:4326</h2>
|
||||
<ul>
|
||||
<li><strong>WGS84</strong>: 전 세계 GPS가 쓰는 <strong>위도·경도 표준</strong>. "지구를 이런 모양·기준으로 본다"는 세계 공통 약속.</li>
|
||||
<li><strong>EPSG</strong>: 세상의 여러 좌표계에 <strong>번호표를 붙여 정리한 목록</strong> (도서관 책번호 같은 것).</li>
|
||||
<li><strong>EPSG:4326</strong> = 그 목록에서 <strong>WGS84(위도·경도)</strong> 에 붙은 번호. 코드에서 <code>'EPSG:4326'</code> 이라 쓰면 "위경도 방식"이란 뜻.</li>
|
||||
</ul>
|
||||
<h2 id="-epsg5186--한국-tm-횡축-메르카토르">▸ EPSG:5186 · 한국 TM (횡축 메르카토르)</h2>
|
||||
<ul>
|
||||
<li>우리나라 전용 <strong>평평한 미터 지도</strong> 좌표계. 위경도(각도)를 <strong>미터(거리)</strong> 로 바꾼 결과가 이 좌표.</li>
|
||||
<li><strong>TM = Transverse Mercator(횡축 메르카토르)</strong>: 둥근 지구에 <strong>원통을 옆으로 눕혀 씌워</strong> 펴는 방법. 원통이 닿는 <strong>세로선(경도) 근처가 가장 정확</strong> → <strong>남북으로 길쭉한 한국에 딱</strong>.</li>
|
||||
</ul>
|
||||
<figure class="fig">
|
||||
<svg viewBox="0 0 700 190" role="img" aria-label="TM 도법 - 원통을 눕혀 씌우기">
|
||||
<text x="130" y="24" text-anchor="middle" font-size="11" fill="#475569" font-weight="bold">보통 메르카토르</text>
|
||||
<ellipse cx="130" cy="105" rx="45" ry="70" fill="none" stroke="#94a3b8" stroke-width="2"></ellipse>
|
||||
<circle cx="130" cy="105" r="42" fill="#dbeafe" stroke="#2563eb"></circle>
|
||||
<line x1="88" y1="105" x2="172" y2="105" stroke="#ef4444" stroke-width="2.5"></line>
|
||||
<text x="130" y="182" text-anchor="middle" font-size="10" fill="#b91c1c">가로(적도) 근처 정확</text>
|
||||
<text x="350" y="105" text-anchor="middle" font-size="24" fill="#64748b">➡ 90° 눕힘 ➡</text>
|
||||
<text x="560" y="24" text-anchor="middle" font-size="11" fill="#475569" font-weight="bold">TM (횡축) — 한국용</text>
|
||||
<ellipse cx="560" cy="105" rx="70" ry="45" fill="none" stroke="#94a3b8" stroke-width="2"></ellipse>
|
||||
<circle cx="560" cy="105" r="42" fill="#dcfce7" stroke="#16a34a"></circle>
|
||||
<line x1="560" y1="63" x2="560" y2="147" stroke="#ef4444" stroke-width="2.5"></line>
|
||||
<text x="560" y="182" text-anchor="middle" font-size="10" fill="#b91c1c">세로(경도127°) 근처 정확</text>
|
||||
</svg>
|
||||
<figcaption>원통이 '닿는 빨간 선' 근처가 가장 정확하다. 한국은 세로로 길어서 세로선에 맞추는 TM을 쓴다.</figcaption>
|
||||
</figure>
|
||||
|
||||
<p><strong>코드의 설정 쪽지 뜻</strong> (<code>+proj=tmerc +lat_0=38 +lon_0=127 +k=1 +x_0=200000 +y_0=600000 +ellps=GRS80</code>):</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr class="header">
|
||||
<th>설정</th>
|
||||
<th>뜻</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="odd">
|
||||
<td><code>proj=tmerc</code></td>
|
||||
<td>TM(횡축 메르카토르) 방식</td>
|
||||
</tr>
|
||||
<tr class="even">
|
||||
<td><code>lat_0=38</code> <code>lon_0=127</code></td>
|
||||
<td>지도 <strong>기준점</strong> = 위도38·경도127 (한국 한가운데)</td>
|
||||
</tr>
|
||||
<tr class="odd">
|
||||
<td><code>k=1</code></td>
|
||||
<td>크기 <strong>1배</strong>(줄임/늘림 없음)</td>
|
||||
</tr>
|
||||
<tr class="even">
|
||||
<td><code>x_0=200000</code> <code>y_0=600000</code></td>
|
||||
<td><strong>false easting/northing</strong> (아래 항목 참고)</td>
|
||||
</tr>
|
||||
<tr class="odd">
|
||||
<td><code>ellps=GRS80</code></td>
|
||||
<td>지구를 <strong>GRS80</strong>(살짝 찌그러진 귤 모양)으로 봄</td>
|
||||
</tr>
|
||||
<tr class="even">
|
||||
<td><code>units=m</code></td>
|
||||
<td>단위 = 미터</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h2 id="-false-easting--false-northing-x_0-y_0">▸ false easting / false northing (x_0, y_0)</h2>
|
||||
<p>기준점에서 서쪽·남쪽으로 가면 좌표가 <strong>마이너스(−)</strong> 가 돼요. 계산이 헷갈리니까, <strong>처음부터 큰 수를 더해</strong> 어디서나 <strong>플러스(+)</strong> 가 되게 해요. (한국은 가로 +20만, 세로 +60만 m)</p>
|
||||
<figure class="fig">
|
||||
<svg viewBox="0 0 700 110" role="img" aria-label="false easting - 마이너스를 없애려 큰 수 더하기">
|
||||
<line x1="40" y1="45" x2="660" y2="45" stroke="#94a3b8" stroke-width="2"></line>
|
||||
<line x1="200" y1="35" x2="200" y2="55" stroke="#111"></line>
|
||||
<text x="200" y="28" text-anchor="middle" font-size="10" fill="#111">기준점 0</text>
|
||||
<text x="110" y="72" text-anchor="middle" font-size="11" fill="#b91c1c">서쪽 = −50000 😵</text>
|
||||
<text x="320" y="72" text-anchor="middle" font-size="11" fill="#166534">동쪽 = +30000</text>
|
||||
<text x="350" y="95" text-anchor="middle" font-size="11" fill="#3730a3">+20만을 더하면 → 서쪽도 150000 (전부 +) 😎</text>
|
||||
</svg>
|
||||
<figcaption>온도에 273을 더해 '절대온도(K)'로 음수를 없애는 것과 같은 아이디어.</figcaption>
|
||||
</figure>
|
||||
|
||||
<h2 id="-높이-3형제--정표고--지오이드고--타원체고-geoidoffset">▸ 높이 3형제 — 정표고 · 지오이드고 · 타원체고 (geoidOffset)</h2>
|
||||
<p>높이를 재는 <strong>기준면이 3가지</strong>라서 서로 변환이 필요해요. 드론 GPS 높이와 지도 높이의 <strong>기준을 맞추려고</strong> <code>geoidOffset</code>(대전≈25.8m)을 더해요.</p>
|
||||
<figure class="fig">
|
||||
<svg viewBox="0 0 700 200" role="img" aria-label="정표고 지오이드고 타원체고">
|
||||
<line x1="60" y1="60" x2="640" y2="60" stroke="#ca8a04" stroke-width="2" stroke-dasharray="6 4"></line>
|
||||
<text x="648" y="63" font-size="10" fill="#854d0e">타원체(GPS 기준, 매끈한 계산용 면)</text>
|
||||
<path d="M60,110 q145,-22 290,0 t290,0" fill="none" stroke="#2563eb" stroke-width="2"></path>
|
||||
<text x="648" y="113" font-size="10" fill="#1e3a8a">지오이드(평균 해수면)</text>
|
||||
<path d="M60,150 q80,-30 160,-8 q120,26 220,-14 q140,-40 340,10" fill="none" stroke="#16a34a" stroke-width="2.5"></path>
|
||||
<text x="120" y="175" font-size="10" fill="#166534">실제 땅(산·건물)</text>
|
||||
<line x1="330" y1="60" x2="330" y2="102" stroke="#a16207" stroke-width="1.5"></line>
|
||||
<text x="336" y="88" font-size="10" fill="#a16207">지오이드고(≈25.8m)</text>
|
||||
<line x1="330" y1="110" x2="330" y2="140" stroke="#15803d" stroke-width="1.5"></line>
|
||||
<text x="336" y="130" font-size="10" fill="#15803d">정표고(지도 높이)</text>
|
||||
</svg>
|
||||
<figcaption>정표고(우리가 아는 '해발 높이') + 지오이드고 = 타원체고(GPS 높이). 코드는 이 변환으로 기준을 맞춘다.</figcaption>
|
||||
</figure>
|
||||
|
||||
<h2 id="-enu-좌표-east-north-up-동북상">▸ ENU 좌표 (East-North-Up, 동·북·상)</h2>
|
||||
<p>한 점(기준원점)을 중심으로 <strong>동쪽(E)·북쪽(N)·위(U)</strong> 세 방향으로 "몇 m"인지 나타내는 방식. 드론을 원점에 놓으면 터널이 "동 +50, 북 +200, 위 −80" 처럼 표현돼요(본문 ②).</p>
|
||||
<figure class="fig">
|
||||
<svg viewBox="0 0 700 180" role="img" aria-label="ENU 동북상 좌표축">
|
||||
<defs><marker id="ax" markerWidth="10" markerHeight="10" refX="7" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7 Z" fill="#334155"></path></marker></defs>
|
||||
<circle cx="300" cy="120" r="4" fill="#111"></circle>
|
||||
<text x="300" y="150" text-anchor="middle" font-size="10" fill="#475569">드론(원점 0,0,0)</text>
|
||||
<line x1="300" y1="120" x2="470" y2="120" stroke="#334155" stroke-width="2" marker-end="url(#ax)"></line>
|
||||
<text x="480" y="124" font-size="11" fill="#b45309" font-weight="bold">E 동쪽</text>
|
||||
<line x1="300" y1="120" x2="300" y2="30" stroke="#334155" stroke-width="2" marker-end="url(#ax)"></line>
|
||||
<text x="285" y="26" font-size="11" fill="#15803d" font-weight="bold">U 위</text>
|
||||
<line x1="300" y1="120" x2="210" y2="70" stroke="#334155" stroke-width="2" marker-end="url(#ax)"></line>
|
||||
<text x="150" y="66" font-size="11" fill="#1e3a8a" font-weight="bold">N 북쪽</text>
|
||||
<text x="430" y="70" font-size="16">🚇</text>
|
||||
<text x="430" y="90" font-size="10" fill="#166534">터널: 동+50, 북+200, 위−80</text>
|
||||
</svg>
|
||||
<figcaption>기준원점(getWorldOrigin)에서 동·북·위로 잰 미터 좌표. 빼기로 '드론 기준 상대위치'를 만든다.</figcaption>
|
||||
</figure>
|
||||
|
||||
<h2 id="-yaw--pitch--roll-카메라가-기운-3방향">▸ yaw · pitch · roll (카메라가 기운 3방향)</h2>
|
||||
<p>카메라(또는 드론)가 향한 방향을 나타내는 <strong>3개의 회전</strong>. 사람 고개 움직임과 같아요.</p>
|
||||
<figure class="fig">
|
||||
<svg viewBox="0 0 700 150" role="img" aria-label="yaw pitch roll">
|
||||
<g transform="translate(120,75)"><text x="0" y="-38" text-anchor="middle" font-size="26">🙂</text>
|
||||
<path d="M-42,8 a42,16 0 0 0 84,0" fill="none" stroke="#2563eb" stroke-width="3"></path><polygon points="42,8 35,2 35,15" fill="#2563eb"></polygon>
|
||||
<text x="0" y="40" text-anchor="middle" font-size="12" font-weight="bold" fill="#2563eb">yaw</text><text x="0" y="57" text-anchor="middle" font-size="10" fill="#64748b">좌우(도리도리)</text></g>
|
||||
<g transform="translate(350,75)"><text x="0" y="-38" text-anchor="middle" font-size="26">🙂</text>
|
||||
<path d="M0,-28 a16,42 0 0 0 0,56" fill="none" stroke="#16a34a" stroke-width="3"></path><polygon points="0,28 -6,21 6,21" fill="#16a34a"></polygon>
|
||||
<text x="0" y="40" text-anchor="middle" font-size="12" font-weight="bold" fill="#16a34a">pitch</text><text x="0" y="57" text-anchor="middle" font-size="10" fill="#64748b">위아래(끄덕끄덕)</text></g>
|
||||
<g transform="translate(580,75)"><text x="0" y="-38" text-anchor="middle" font-size="26">🙂</text>
|
||||
<path d="M-28,0 a28,28 0 1 1 7,18" fill="none" stroke="#db2777" stroke-width="3"></path><polygon points="-21,18 -28,12 -13,11" fill="#db2777"></polygon>
|
||||
<text x="0" y="40" text-anchor="middle" font-size="12" font-weight="bold" fill="#db2777">roll</text><text x="0" y="57" text-anchor="middle" font-size="10" fill="#64748b">갸웃(갸우뚱)</text></g>
|
||||
</svg>
|
||||
</figure>
|
||||
|
||||
<h2 id="-회전행렬-rotation-matrix">▸ 회전행렬 (Rotation Matrix)</h2>
|
||||
<p>위 3개 회전(yaw·pitch·roll)을 <strong>하나의 작은 숫자표(3×3)</strong> 로 미리 합쳐 둔 것. 이 표를 위치에 <strong>한 번 곱하면</strong> 세 방향 회전이 <strong>동시에</strong> 적용돼요. (문서의 "방향 돌리기 만능 양념장")</p>
|
||||
<figure class="fig">
|
||||
<svg viewBox="0 0 700 130" role="img" aria-label="세 회전을 한 숫자표로 합치기">
|
||||
<rect x="30" y="45" width="70" height="34" rx="6" fill="#eff6ff" stroke="#2563eb"></rect><text x="65" y="67" text-anchor="middle" font-size="12" fill="#1e3a8a">yaw</text>
|
||||
<text x="110" y="67" font-size="16">+</text>
|
||||
<rect x="130" y="45" width="70" height="34" rx="6" fill="#f0fdf4" stroke="#16a34a"></rect><text x="165" y="67" text-anchor="middle" font-size="12" fill="#166534">pitch</text>
|
||||
<text x="210" y="67" font-size="16">+</text>
|
||||
<rect x="230" y="45" width="70" height="34" rx="6" fill="#fdf2f8" stroke="#db2777"></rect><text x="265" y="67" text-anchor="middle" font-size="12" fill="#9d174d">roll</text>
|
||||
<text x="335" y="67" font-size="22" fill="#64748b">➡</text>
|
||||
<rect x="380" y="30" width="120" height="66" rx="6" fill="#fffbe6" stroke="#ca8a04"></rect>
|
||||
<text x="440" y="52" text-anchor="middle" font-size="10" fill="#854d0e">3×3 숫자표</text>
|
||||
<text x="440" y="70" text-anchor="middle" font-size="10" fill="#854d0e">(회전행렬)</text>
|
||||
<text x="440" y="86" text-anchor="middle" font-size="9" fill="#a16207">R_w2c</text>
|
||||
<text x="520" y="67" font-size="16">×</text>
|
||||
<rect x="540" y="45" width="120" height="34" rx="6" fill="#f1f5f9" stroke="#475569"></rect><text x="600" y="67" text-anchor="middle" font-size="10" fill="#334155">위치 → 돌린 위치</text>
|
||||
</svg>
|
||||
<figcaption>3번 돌릴 걸 표 1개로 합쳐 한 번에 곱한다 → 빠르고 실수 없음. 코드: buildRotation + applyRw2c.</figcaption>
|
||||
</figure>
|
||||
|
||||
<h2 id="-카메라-좌표-xc-yc-zc">▸ 카메라 좌표 (Xc, Yc, Zc)</h2>
|
||||
<p>회전까지 마친 뒤 얻는, <strong>카메라 눈 기준</strong> 좌표. <code>Zc</code>=앞쪽 거리, <code>Xc</code>=좌우, <code>Yc</code>=상하. <code>Zc</code>가 0보다 커야(앞에 있어야) 화면에 보여요. 다음 단계(핀홀)에서 이 값을 <code>Xc/Zc</code>, <code>Yc/Zc</code>로 나눠 써요.</p>
|
||||
<h2 id="-핀홀-카메라-모델--초점거리--센서">▸ 핀홀 카메라 모델 · 초점거리 · 센서</h2>
|
||||
<p><strong>바늘구멍 사진기</strong> 원리. 작은 구멍(렌즈)을 지나며 3D가 화면에 맺혀요. <strong>멀면 작게, 가까우면 크게</strong>(원근).</p>
|
||||
<figure class="fig">
|
||||
<svg viewBox="0 0 700 190" role="img" aria-label="핀홀 카메라 - 빛이 구멍 지나 거꾸로 맺힘">
|
||||
<line x1="90" y1="30" x2="90" y2="150" stroke="#16a34a" stroke-width="6"></line><polygon points="90,30 84,44 96,44" fill="#16a34a"></polygon>
|
||||
<text x="90" y="170" text-anchor="middle" font-size="10" fill="#166534">터널(큼)</text>
|
||||
<line x1="360" y1="18" x2="360" y2="162" stroke="#94a3b8" stroke-width="6"></line><circle cx="360" cy="90" r="5" fill="#111"></circle>
|
||||
<text x="360" y="180" text-anchor="middle" font-size="10" fill="#475569">바늘구멍(렌즈)</text>
|
||||
<line x1="90" y1="30" x2="560" y2="138.6" stroke="#f59e0b" stroke-width="1.3"></line>
|
||||
<line x1="90" y1="150" x2="560" y2="41.4" stroke="#f59e0b" stroke-width="1.3"></line>
|
||||
<line x1="560" y1="41.4" x2="560" y2="138.6" stroke="#ef4444" stroke-width="5"></line><polygon points="560,138.6 554,124.6 566,124.6" fill="#ef4444"></polygon>
|
||||
<text x="560" y="170" text-anchor="middle" font-size="10" fill="#b91c1c">맺힌 상(작고 거꾸로)</text>
|
||||
</svg>
|
||||
<figcaption>모든 빛이 구멍 한 점을 지나 X자로 교차 → 화면엔 작고 거꾸로 맺힘. '초점거리(focal length)'가 클수록(망원) 크게 보인다.</figcaption>
|
||||
</figure>
|
||||
|
||||
<ul>
|
||||
<li><strong>초점거리(focalLen)</strong>: 렌즈가 얼마나 '당겨 찍나'(망원↔︎광각). 클수록 화면에서 크게.</li>
|
||||
<li><strong>센서(sensorW/H)</strong>: 카메라 필름 크기. 초점거리와 센서 비율이 화각을 정함(코드 <code>f/sW</code>, <code>f/sH</code>).</li>
|
||||
</ul>
|
||||
<h2 id="-정규좌표-01">▸ 정규좌표 (0~1)</h2>
|
||||
<p>화면 위치를 픽셀(예: 1920) 대신 <strong>비율(0~1)</strong> 로 나타낸 것. <strong>0=왼쪽/위, 1=오른쪽/아래.</strong> 화면 크기가 달라져도 그대로 쓸 수 있어 편해요. (예: 가로 0.6 = 화면의 60% 지점)</p>
|
||||
<h2 id="-보간-interpolation">▸ 보간 (Interpolation)</h2>
|
||||
<p>두 값 <strong>사이의 중간값</strong>을 계산해 채우는 것. 사진이 30장뿐이어도 사이사이를 상상해 채워 <strong>부드럽게</strong> 움직여요.</p>
|
||||
<figure class="fig">
|
||||
<svg viewBox="0 0 700 120" role="img" aria-label="보간 - 두 점 사이 중간 채우기">
|
||||
<circle cx="120" cy="80" r="7" fill="#2563eb"></circle><text x="120" y="105" text-anchor="middle" font-size="10" fill="#1e3a8a">사진1</text>
|
||||
<circle cx="560" cy="40" r="7" fill="#2563eb"></circle><text x="560" y="30" text-anchor="middle" font-size="10" fill="#1e3a8a">사진2</text>
|
||||
<line x1="120" y1="80" x2="560" y2="40" stroke="#cbd5e1" stroke-dasharray="4 4"></line>
|
||||
<circle cx="230" cy="70" r="4" fill="#ef4444"></circle><circle cx="340" cy="60" r="4" fill="#ef4444"></circle><circle cx="450" cy="50" r="4" fill="#ef4444"></circle>
|
||||
<text x="340" y="95" text-anchor="middle" font-size="10" fill="#b91c1c">← 중간을 채운 위치(보간) →</text>
|
||||
</svg>
|
||||
<figcaption>사진1·2 사이 몇 % 지점인지(frac) 계산해 중간 위치를 만든다. 코드: poseAt.</figcaption>
|
||||
</figure>
|
||||
|
||||
<h2 id="-이동평균--ema-지수이동평균">▸ 이동평균 · EMA (지수이동평균)</h2>
|
||||
<p>여러 값을 <strong>평균 내</strong> 들쭉날쭉(떨림)을 매끈하게 만드는 방법.</p>
|
||||
<ul>
|
||||
<li><strong>이동평균</strong>: 앞뒤 몇 개를 평균 (코드 <code>smoothFrame</code>)</li>
|
||||
<li><strong>EMA</strong>: 최근 값에 더 무게를 둔 평균, 반응이 빠름 (코드 <code>smoothStep</code>)</li>
|
||||
</ul>
|
||||
<figure class="fig">
|
||||
<svg viewBox="0 0 700 130" role="img" aria-label="떨리는 값 vs 평활한 값">
|
||||
<polyline points="40,70 80,40 120,95 160,45 200,90 240,50 280,85 320,55 360,80" fill="none" stroke="#f87171" stroke-width="2"></polyline>
|
||||
<text x="200" y="120" text-anchor="middle" font-size="10" fill="#b91c1c">평활 전: 부들부들 떨림</text>
|
||||
<path d="M400,70 q80,-8 160,-6 t120,-4" fill="none" stroke="#16a34a" stroke-width="3"></path>
|
||||
<text x="560" y="120" text-anchor="middle" font-size="10" fill="#166534">평활 후: 매끈</text>
|
||||
<text x="378" y="66" font-size="18" fill="#64748b">➡</text>
|
||||
</svg>
|
||||
<figcaption>드론 흔들림으로 튀는 위치를 평균으로 눌러 이름표가 안 떨리게 한다.</figcaption>
|
||||
</figure>
|
||||
|
||||
<h2 id="-requestanimationframe-raf">▸ requestAnimationFrame (RAF)</h2>
|
||||
<p>브라우저에게 <strong>"화면 새로 그릴 때마다 이 일을 해줘"</strong> 라고 부탁하는 명령. 보통 1초에 약 60번 실행돼요. 그래서 이름표 위치를 매번 다시 계산·그리기 해 부드럽게 따라다녀요.</p>
|
||||
<h2 id="-canvas-2d">▸ Canvas 2D</h2>
|
||||
<p>웹페이지 안의 <strong>그림판(도화지)</strong>. 여기에 선·글자를 직접 그려요. 우리는 중심선·측점·POI 이름표를 매 프레임 이 캔버스에 그려요. (선명하고 빠름)</p>
|
||||
<h2 id="-fps--프레임">▸ fps · 프레임</h2>
|
||||
<ul>
|
||||
<li><strong>프레임</strong> = 영상의 <strong>낱장 사진</strong> 한 장.</li>
|
||||
<li><strong>fps(frames per second)</strong> = <strong>1초에 몇 장</strong> 넘기나. 예: 30fps = 1초에 30장.</li>
|
||||
<li>프레임 번호를 시간으로 바꾸려면 fps가 필요해요: <code>시간 = 프레임번호 ÷ fps</code> (본문 ⑧).</li>
|
||||
</ul>
|
||||
<h2 id="-requestidlecallback">▸ requestIdleCallback</h2>
|
||||
<p>브라우저가 <strong>한가한 틈</strong>에 무거운 일을 시키는 명령. 무거운 "어떤 라벨을 보여줄지" 준비 작업을 여기서 미리 해 두고, 매 프레임(RAF)에는 가벼운 그리기만 → 60번/초에도 안 버벅여요.</p>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user