기존 GhiVideo 저장소 HEAD의 트래킹 소스 362개 파일을 복제. (node_modules·storage·빌드 산출물·대용량 미디어는 .gitignore 규칙대로 제외) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
857 lines
58 KiB
HTML
857 lines
58 KiB
HTML
<!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>
|
||
<h2 id="드론-gps--영상--poi-좌표를-영상-위-정확한-위치에--그리고-프레임에서-측점스테이션으로">드론 GPS · 영상 · POI 좌표를 영상 위 정확한 위치에 — 그리고 프레임에서 측점(스테이션)으로</h2>
|
||
<blockquote>
|
||
<p>작성일: 2026-06-30 · 발표용 요약 문서 슬라이드 구분은 <code>---</code> 입니다 (Marp/reveal.js 변환 가능). 각 장은 "그림 → 핵심 → 코드 근거" 순.</p>
|
||
</blockquote>
|
||
<style>
|
||
figure.fig{margin:18px 0;text-align:center;page-break-inside:avoid;break-inside:avoid;}
|
||
figure.fig svg{width:100%;max-width:680px;height:auto;border:1px solid #e7e0d2;border-radius:10px;background:#fffdf8;}
|
||
figure.fig figcaption{font-size:0.88em;color:#777;margin-top:6px;}
|
||
text{font-family:'Malgun Gothic','Hancom Gothic',sans-serif;}
|
||
h2{border-bottom:2px solid #e7e0d2;padding-bottom:4px;}
|
||
.lead{background:#f8f6ef;border-left:4px solid #b45309;padding:10px 14px;border-radius:6px;}
|
||
</style>
|
||
|
||
<hr />
|
||
<h2 id="목차">목차</h2>
|
||
<table>
|
||
<thead>
|
||
<tr class="header">
|
||
<th>🟦 공간 정합</th>
|
||
<th>🟩 시간 동기화</th>
|
||
<th>🟧 표시 안정</th>
|
||
<th>🟦 측점 전환</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr class="odd">
|
||
<td>지도좌표 → 화면픽셀</td>
|
||
<td>영상시각 ↔︎ 드론프레임</td>
|
||
<td>떨림·튐 제거</td>
|
||
<td>프레임 → 측점(공간)</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
<ol type="1">
|
||
<li>우리가 풀어야 했던 문제</li>
|
||
<li>입력 데이터 3종 — GPS · 영상 · POI</li>
|
||
<li>전체 파이프라인 한 장</li>
|
||
<li><strong>핵심 ①</strong> 좌표 정합 — 지도 좌표를 화면 픽셀로 (투영)</li>
|
||
<li><strong>핵심 ②</strong> 시간 동기화 — 영상 시각과 드론 프레임 맞추기</li>
|
||
<li><strong>핵심 ③</strong> 라벨 안정화 — 떨림·튐 제거</li>
|
||
<li><strong>핵심 ④</strong> 프레임 기반 → 측점(스테이션) 기반 전환</li>
|
||
<li>정확도 보정 도구 (DEM · 드래그 · 세로화각)</li>
|
||
<li>정리 — 무엇을, 어떻게</li>
|
||
</ol>
|
||
<hr />
|
||
<h2 id="1-우리가-풀어야-했던-문제">1. 우리가 풀어야 했던 문제</h2>
|
||
<p class="lead">드론이 철도 노선을 따라 비행하며 찍은 영상 위에, <b>교량·터널·역사·지장물(POI)의 이름표를 실제 위치에 정확히</b> 띄우고 싶다.</p>
|
||
|
||
<figure class="fig">
|
||
<svg viewBox="0 0 680 300" role="img" aria-label="드론 영상 위 라벨 예시">
|
||
<!-- video frame -->
|
||
<rect x="20" y="20" width="640" height="230" rx="10" fill="#0b1020" stroke="#334155" stroke-width="2"></rect>
|
||
<!-- sky gradient -->
|
||
<defs>
|
||
<linearGradient id="sky" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#1e3a5f"></stop><stop offset="1" stop-color="#0b1020"></stop></linearGradient>
|
||
<marker id="m1" markerWidth="9" markerHeight="9" refX="6" refY="3" orient="auto"><path d="M0,0 L6,3 L0,6 Z" fill="#f59e0b"></path></marker>
|
||
</defs>
|
||
<rect x="22" y="22" width="636" height="120" fill="url(#sky)"></rect>
|
||
<!-- rails -->
|
||
<polygon points="250,250 430,250 360,120 320,120" fill="#1c2533" stroke="#475569"></polygon>
|
||
<line x1="285" y1="250" x2="338" y2="120" stroke="#94a3b8" stroke-width="2"></line>
|
||
<line x1="395" y1="250" x2="342" y2="120" stroke="#94a3b8" stroke-width="2"></line>
|
||
<g stroke="#64748b" stroke-width="1.5">
|
||
<line x1="300" y1="210" x2="380" y2="210"></line><line x1="312" y1="180" x2="368" y2="180"></line><line x1="322" y1="155" x2="358" y2="155"></line></g>
|
||
<!-- bridge label -->
|
||
<circle cx="175" cy="120" r="5" fill="#f59e0b"></circle>
|
||
<rect x="120" y="78" width="110" height="26" rx="5" fill="#f59e0b"></rect>
|
||
<text x="175" y="96" text-anchor="middle" font-size="13" font-weight="bold" fill="#1a1303">🌉 회덕제1가도교</text>
|
||
<line x1="175" y1="104" x2="175" y2="116" stroke="#f59e0b" stroke-width="1.5" marker-end="url(#m1)"></line>
|
||
<!-- tunnel label -->
|
||
<circle cx="500" cy="120" r="5" fill="#38bdf8"></circle>
|
||
<rect x="455" y="78" width="92" height="26" rx="5" fill="#38bdf8"></rect>
|
||
<text x="501" y="96" text-anchor="middle" font-size="13" font-weight="bold" fill="#06283d">🚇 법동터널</text>
|
||
<line x1="501" y1="104" x2="501" y2="116" stroke="#38bdf8" stroke-width="1.5"></line>
|
||
<!-- HUD -->
|
||
<rect x="34" y="214" width="320" height="24" rx="5" fill="#000" opacity="0.55"></rect>
|
||
<text x="44" y="231" font-size="12" fill="#e2e8f0">GPS 36.334, 127.456 · 고도 123m · 측점 157K970</text>
|
||
<!-- compass -->
|
||
<circle cx="615" cy="60" r="24" fill="#0b1020" stroke="#64748b"></circle>
|
||
<polygon points="615,42 609,60 621,60" fill="#f87171"></polygon><text x="615" y="38" text-anchor="middle" font-size="9" fill="#f87171">N</text>
|
||
<text x="340" y="278" text-anchor="middle" font-size="12" fill="#555">드론 영상 + 실제 위치에 붙는 이름표 + 위치 HUD</text>
|
||
</svg>
|
||
<figcaption>그림 1. 목표 — 흔들리는 드론 영상 위에, 지도 좌표만 가진 POI를 "진짜 그 자리"에 표시.</figcaption>
|
||
</figure>
|
||
|
||
<p><strong>난이도 4가지</strong> → 그래서 <strong>세 가지 정합</strong>이 필요합니다.</p>
|
||
<table>
|
||
<thead>
|
||
<tr class="header">
|
||
<th>난이도</th>
|
||
<th>무엇이 어려운가</th>
|
||
<th>해결 정합</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr class="odd">
|
||
<td>드론 흔들림</td>
|
||
<td>yaw/pitch/roll·고도 변화·빠른 이동</td>
|
||
<td>🟦 공간 + 🟧 안정</td>
|
||
</tr>
|
||
<tr class="even">
|
||
<td>POI는 지도좌표만</td>
|
||
<td>"화면 어디"인지 모름</td>
|
||
<td>🟦 공간(투영)</td>
|
||
</tr>
|
||
<tr class="odd">
|
||
<td>시간 어긋남</td>
|
||
<td>영상 시각 ↔︎ 드론 데이터 시각</td>
|
||
<td>🟩 시간</td>
|
||
</tr>
|
||
<tr class="even">
|
||
<td>매끄러움</td>
|
||
<td>60fps·떨림 없이</td>
|
||
<td>🟧 안정</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
<hr />
|
||
<h2 id="2-입력-데이터-3종">2. 입력 데이터 3종</h2>
|
||
<figure class="fig">
|
||
<svg viewBox="0 0 680 270" role="img" aria-label="입력 3종 결합">
|
||
<defs><marker id="m2" markerWidth="10" markerHeight="10" refX="7" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7 Z" fill="#b45309"></path></marker></defs>
|
||
<!-- 1 drone log -->
|
||
<rect x="30" y="30" width="170" height="120" rx="10" fill="#f5f3ff" stroke="#7c3aed" stroke-width="1.5"></rect>
|
||
<text x="115" y="54" text-anchor="middle" font-size="14" font-weight="bold" fill="#5b21b6">① 드론 로그</text>
|
||
<text x="115" y="74" text-anchor="middle" font-size="11" fill="#6d28d9">프레임별 비행 CSV</text>
|
||
<g font-size="10" fill="#4c1d95" font-family="monospace">
|
||
<text x="46" y="98">frame lat yaw</text>
|
||
<text x="46" y="114"> 0 36.33 12°</text>
|
||
<text x="46" y="130"> 1 36.33 13°</text>
|
||
<text x="46" y="146"> … alt·pitch·roll</text></g>
|
||
<!-- 2 video -->
|
||
<rect x="255" y="30" width="170" height="120" rx="10" fill="#eff6ff" stroke="#2563eb" stroke-width="1.5"></rect>
|
||
<text x="340" y="54" text-anchor="middle" font-size="14" font-weight="bold" fill="#1e3a8a">② 영상</text>
|
||
<text x="340" y="74" text-anchor="middle" font-size="11" fill="#1d4ed8">mp4/webm 주행영상</text>
|
||
<rect x="295" y="86" width="90" height="50" rx="4" fill="#0b1020"></rect>
|
||
<polygon points="332,100 332,122 352,111" fill="#fff"></polygon>
|
||
<!-- 3 poi -->
|
||
<rect x="480" y="30" width="170" height="120" rx="10" fill="#ecfdf5" stroke="#16a34a" stroke-width="1.5"></rect>
|
||
<text x="565" y="54" text-anchor="middle" font-size="14" font-weight="bold" fill="#14532d">③ POI / 측점</text>
|
||
<text x="565" y="74" text-anchor="middle" font-size="11" fill="#15803d">KMZ(1순위)+측점CSV</text>
|
||
<g font-size="10" fill="#166534" font-family="monospace">
|
||
<text x="496" y="98">🌉 교량 (lat,lon,z)</text>
|
||
<text x="496" y="114">🚇 터널 (lat,lon,z)</text>
|
||
<text x="496" y="130">📍 측점 157K970</text></g>
|
||
<!-- merge -->
|
||
<line x1="115" y1="150" x2="320" y2="195" stroke="#b45309" stroke-width="1.5" marker-end="url(#m2)"></line>
|
||
<line x1="340" y1="150" x2="340" y2="195" stroke="#b45309" stroke-width="1.5" marker-end="url(#m2)"></line>
|
||
<line x1="565" y1="150" x2="360" y2="195" stroke="#b45309" stroke-width="1.5" marker-end="url(#m2)"></line>
|
||
<rect x="200" y="205" width="280" height="40" rx="20" fill="#fff7e6" stroke="#f59e0b" stroke-width="2"></rect>
|
||
<text x="340" y="230" text-anchor="middle" font-size="14" font-weight="bold" fill="#b45309">이 셋을 시간·공간으로 묶는다</text>
|
||
</svg>
|
||
<figcaption>그림 2. 드론 로그(언제·어디·어느 방향) + 영상 + 지도 POI를 결합.</figcaption>
|
||
</figure>
|
||
|
||
<hr />
|
||
<h2 id="3-전체-파이프라인-한-장">3. 전체 파이프라인 한 장</h2>
|
||
<figure class="fig">
|
||
<svg viewBox="0 0 680 420" role="img" aria-label="전체 파이프라인">
|
||
<defs><marker id="m3" markerWidth="10" markerHeight="10" refX="7" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7 Z" fill="#64748b"></path></marker></defs>
|
||
<!-- lane labels -->
|
||
<rect x="14" y="14" width="14" height="392" rx="4" fill="#16a34a" opacity="0.15"></rect>
|
||
<!-- step: time -->
|
||
<rect x="180" y="20" width="320" height="46" rx="8" fill="#ecfdf5" stroke="#16a34a" stroke-width="1.5"></rect>
|
||
<text x="340" y="40" text-anchor="middle" font-size="13" font-weight="bold" fill="#14532d">🟩 영상 재생 시각 t</text>
|
||
<text x="340" y="58" text-anchor="middle" font-size="11" fill="#15803d">smoothTimeRef: 60fps 단조보간</text>
|
||
<line x1="340" y1="66" x2="340" y2="84" stroke="#64748b" stroke-width="1.6" marker-end="url(#m3)"></line>
|
||
<rect x="180" y="86" width="320" height="40" rx="8" fill="#ecfdf5" stroke="#16a34a" stroke-width="1.2"></rect>
|
||
<text x="340" y="111" text-anchor="middle" font-size="12" fill="#166534">t → 드론 프레임 보간 → 그 순간의 위치·자세(pose)</text>
|
||
<line x1="340" y1="126" x2="340" y2="142" stroke="#64748b" stroke-width="1.6" marker-end="url(#m3)"></line>
|
||
<!-- projection box -->
|
||
<rect x="120" y="144" width="440" height="120" rx="10" fill="#eff6ff" stroke="#2563eb" stroke-width="1.8"></rect>
|
||
<text x="340" y="166" text-anchor="middle" font-size="13" font-weight="bold" fill="#1e3a8a">🟦 좌표 정합 (투영) — 4단계</text>
|
||
<g font-size="12" fill="#1e40af">
|
||
<text x="340" y="190" text-anchor="middle">POI(위경도·표고) → ENU(평면 미터)</text>
|
||
<text x="340" y="212" text-anchor="middle">→ 카메라 좌표 (yaw/pitch/roll 회전)</text>
|
||
<text x="340" y="234" text-anchor="middle">→ 화면 정규픽셀(0~1) [핀홀 카메라]</text>
|
||
<text x="340" y="256" text-anchor="middle">→ object-fit:cover 보정 → 실제 화면 px</text></g>
|
||
<line x1="340" y1="264" x2="340" y2="280" stroke="#64748b" stroke-width="1.6" marker-end="url(#m3)"></line>
|
||
<!-- smoothing -->
|
||
<rect x="180" y="282" width="320" height="40" rx="8" fill="#fff7ed" stroke="#d97706" stroke-width="1.5"></rect>
|
||
<text x="340" y="307" text-anchor="middle" font-size="12" fill="#9a3412">🟧 라벨 평활(One Euro) + 이상치 거부 → 떨림 제거</text>
|
||
<line x1="340" y1="322" x2="340" y2="338" stroke="#64748b" stroke-width="1.6" marker-end="url(#m3)"></line>
|
||
<!-- render -->
|
||
<rect x="180" y="340" width="320" height="40" rx="8" fill="#fff" stroke="#23272e" stroke-width="1.5"></rect>
|
||
<text x="340" y="365" text-anchor="middle" font-size="12" font-weight="bold" fill="#23272e">Canvas RAF 60fps 렌더 → 영상 위 이름표</text>
|
||
<!-- station branch -->
|
||
<line x1="500" y1="360" x2="600" y2="360" stroke="#0891b2" stroke-width="1.6"></line>
|
||
<line x1="600" y1="360" x2="600" y2="395" stroke="#0891b2" stroke-width="1.6"></line>
|
||
<rect x="430" y="392" width="240" height="22" rx="6" fill="#ecfeff" stroke="#0891b2"></rect>
|
||
<text x="550" y="407" text-anchor="middle" font-size="10.5" fill="#155e75">🟦 GPS→측점 투영 → 하단 측점바 커서</text>
|
||
</svg>
|
||
<figcaption>그림 3. 시각 t → pose 보간 → 4단계 투영 → 평활 → 렌더. 동시에 GPS는 측점값으로 투영되어 측점바로.</figcaption>
|
||
</figure>
|
||
|
||
<hr />
|
||
<h2 id="4-핵심-①-좌표-정합--지도-좌표를-화면-픽셀로">4. 핵심 ① 좌표 정합 — 지도 좌표를 화면 픽셀로</h2>
|
||
<p class="lead">"POI는 위경도만 안다. 그게 <b>지금 화면 어디</b>에 보이는가?" — 이게 이 프로그램의 심장. <b>4단계 투영</b>으로 푼다.</p>
|
||
|
||
<figure class="fig">
|
||
<svg viewBox="0 0 680 150" role="img" aria-label="4단계 투영 흐름">
|
||
<defs><marker id="m4" markerWidth="10" markerHeight="10" refX="7" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7 Z" fill="#2563eb"></path></marker></defs>
|
||
<g text-anchor="middle">
|
||
<rect x="14" y="50" width="110" height="54" rx="8" fill="#ecfdf5" stroke="#16a34a"></rect><text x="69" y="74" font-size="12" font-weight="bold" fill="#14532d">위경도+표고</text><text x="69" y="92" font-size="10" fill="#166534">POI 🌉 (지도)</text>
|
||
<rect x="150" y="50" width="110" height="54" rx="8" fill="#eff6ff" stroke="#2563eb"></rect><text x="205" y="72" font-size="12" font-weight="bold" fill="#1e3a8a">평면 미터</text><text x="205" y="90" font-size="10" fill="#1d4ed8">ENU · 1단계</text>
|
||
<rect x="286" y="50" width="110" height="54" rx="8" fill="#eff6ff" stroke="#2563eb"></rect><text x="341" y="72" font-size="12" font-weight="bold" fill="#1e3a8a">카메라 좌표</text><text x="341" y="90" font-size="10" fill="#1d4ed8">회전 · 2단계</text>
|
||
<rect x="422" y="50" width="110" height="54" rx="8" fill="#eff6ff" stroke="#2563eb"></rect><text x="477" y="72" font-size="12" font-weight="bold" fill="#1e3a8a">화면 0~1</text><text x="477" y="90" font-size="10" fill="#1d4ed8">핀홀 · 3단계</text>
|
||
<rect x="558" y="50" width="108" height="54" rx="8" fill="#fff" stroke="#23272e" stroke-width="1.5"></rect><text x="612" y="72" font-size="12" font-weight="bold" fill="#23272e">화면 px</text><text x="612" y="90" font-size="10" fill="#555">cover · 4단계</text>
|
||
</g>
|
||
<line x1="124" y1="77" x2="148" y2="77" stroke="#2563eb" stroke-width="1.5" marker-end="url(#m4)"></line>
|
||
<line x1="260" y1="77" x2="284" y2="77" stroke="#2563eb" stroke-width="1.5" marker-end="url(#m4)"></line>
|
||
<line x1="396" y1="77" x2="420" y2="77" stroke="#2563eb" stroke-width="1.5" marker-end="url(#m4)"></line>
|
||
<line x1="532" y1="77" x2="556" y2="77" stroke="#2563eb" stroke-width="1.5" marker-end="url(#m4)"></line>
|
||
</svg>
|
||
<figcaption>그림 4. 4단계 투영 파이프라인. 파일: client/src/utils/geoProjection.ts</figcaption>
|
||
</figure>
|
||
|
||
<hr />
|
||
<h3 id="4-1-둥근-지구를-평평하게-enu-변환">4-1. 둥근 지구를 평평하게 (ENU 변환)</h3>
|
||
<p>지구는 둥글어 거리 계산이 어렵습니다. 노선 한 구간만 잘라 <strong>평평한 모눈종이(미터 단위)</strong> 로 폅니다.</p>
|
||
<figure class="fig">
|
||
<svg viewBox="0 0 680 200" role="img" aria-label="ENU 변환">
|
||
<defs><marker id="m41" markerWidth="9" markerHeight="9" refX="6" refY="3" orient="auto"><path d="M0,0 L6,3 L0,6 Z" fill="#6b7280"></path></marker></defs>
|
||
<!-- globe -->
|
||
<circle cx="110" cy="100" r="55" fill="#dbeafe" stroke="#2563eb"></circle>
|
||
<ellipse cx="110" cy="100" rx="55" ry="20" fill="none" stroke="#93c5fd"></ellipse>
|
||
<ellipse cx="110" cy="100" rx="20" ry="55" fill="none" stroke="#93c5fd"></ellipse>
|
||
<circle cx="126" cy="80" r="5" fill="#dc2626"></circle>
|
||
<text x="110" y="175" text-anchor="middle" font-size="11" fill="#1e3a8a">둥근 지구 (위경도)</text>
|
||
<text x="235" y="95" font-size="13" fill="#166534" font-weight="bold">EPSG:5186</text>
|
||
<text x="235" y="112" font-size="11" fill="#166534">TM 투영</text>
|
||
<line x1="172" y1="100" x2="318" y2="100" stroke="#16a34a" stroke-width="2.5" marker-end="url(#m41)"></line>
|
||
<!-- grid -->
|
||
<g stroke="#e5e7eb"><line x1="370" y1="40" x2="370" y2="170"></line><line x1="430" y1="40" x2="430" y2="170"></line><line x1="490" y1="40" x2="490" y2="170"></line><line x1="550" y1="40" x2="550" y2="170"></line><line x1="610" y1="40" x2="610" y2="170"></line>
|
||
<line x1="350" y1="60" x2="650" y2="60"></line><line x1="350" y1="100" x2="650" y2="100"></line><line x1="350" y1="140" x2="650" y2="140"></line></g>
|
||
<line x1="370" y1="160" x2="650" y2="160" stroke="#6b7280" stroke-width="2" marker-end="url(#m41)"></line>
|
||
<line x1="370" y1="160" x2="370" y2="40" stroke="#6b7280" stroke-width="2" marker-end="url(#m41)"></line>
|
||
<text x="635" y="178" font-size="11" fill="#6b7280">E 동(m)</text>
|
||
<text x="345" y="48" font-size="11" fill="#6b7280">N 북(m)</text>
|
||
<circle cx="370" cy="160" r="4" fill="#16a34a"></circle><text x="376" y="176" font-size="10" fill="#14532d">기준점(0,0,0)</text>
|
||
<circle cx="490" cy="76" r="6" fill="#b45309"></circle>
|
||
<text x="500" y="72" font-size="11" font-weight="bold" fill="#b45309">POI (E=120, N=300, U=-5)</text>
|
||
</svg>
|
||
<figcaption>그림 5. E/N = 동·북 몇 m, U = 기준점 대비 상대 높이(alt − refAlt).</figcaption>
|
||
</figure>
|
||
|
||
<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><span class="co">// geoToEnu (91–97)</span></span>
|
||
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a>const [e<span class="op">,</span> n] <span class="op">=</span> <span class="fu">latLonToTM</span>(lat<span class="op">,</span> lon)<span class="op">;</span> <span class="co">// 위경도 → 평면 미터</span></span>
|
||
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a>return [e<span class="op">,</span> n<span class="op">,</span> alt <span class="op">-</span> refAlt]<span class="op">;</span></span></code></pre></div>
|
||
<hr />
|
||
<h3 id="4-2-드론이-보는-방향으로-회전-카메라-좌표">4-2. 드론이 보는 방향으로 회전 (카메라 좌표)</h3>
|
||
<p>세상 좌표(E/N/U)를 드론의 <strong>yaw·pitch·roll</strong> 회전행렬에 곱해 "카메라가 보는 좌표(Xc, Yc, Zc)"로 바꿉니다.</p>
|
||
<figure class="fig">
|
||
<svg viewBox="0 0 680 210" role="img" aria-label="세상→카메라 회전">
|
||
<defs><marker id="m42" markerWidth="10" markerHeight="10" refX="7" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7 Z" fill="#7c3aed"></path></marker></defs>
|
||
<!-- world frame -->
|
||
<text x="150" y="30" text-anchor="middle" font-size="12" font-weight="bold" fill="#334155">세상 기준 (북 고정)</text>
|
||
<line x1="80" y1="150" x2="240" y2="150" stroke="#6b7280" stroke-width="1.6"></line><text x="248" y="154" font-size="11" fill="#6b7280">E</text>
|
||
<line x1="150" y1="170" x2="150" y2="60" stroke="#6b7280" stroke-width="1.6"></line><text x="156" y="62" font-size="11" fill="#6b7280">N</text>
|
||
<circle cx="150" cy="150" r="8" fill="#7c3aed"></circle><text x="150" y="186" text-anchor="middle" font-size="10" fill="#5b21b6">🚁 북쪽 봄</text>
|
||
<circle cx="210" cy="95" r="5" fill="#b45309"></circle><text x="218" y="92" font-size="10" fill="#b45309">POI</text>
|
||
<!-- arrow -->
|
||
<line x1="280" y1="120" x2="370" y2="120" stroke="#7c3aed" stroke-width="2.5" marker-end="url(#m42)"></line>
|
||
<text x="325" y="110" text-anchor="middle" font-size="11" fill="#5b21b6">회전</text>
|
||
<!-- camera frame -->
|
||
<text x="520" y="30" text-anchor="middle" font-size="12" font-weight="bold" fill="#334155">드론이 보는 기준 (정면이 기준)</text>
|
||
<circle cx="450" cy="150" r="8" fill="#7c3aed"></circle><text x="450" y="186" text-anchor="middle" font-size="10" fill="#5b21b6">🚁</text>
|
||
<line x1="450" y1="150" x2="610" y2="80" stroke="#16a34a" stroke-width="1.8" marker-end="url(#m42)"></line><text x="612" y="78" font-size="11" fill="#15803d">앞(Zc)</text>
|
||
<line x1="450" y1="150" x2="600" y2="160" stroke="#0891b2" stroke-width="1.8" marker-end="url(#m42)"></line><text x="604" y="166" font-size="11" fill="#155e75">옆(side)</text>
|
||
<circle cx="556" cy="108" r="5" fill="#b45309"></circle><text x="500" y="100" font-size="10" fill="#b45309">POI (앞 50m, 옆 8m)</text>
|
||
</svg>
|
||
<figcaption>그림 6. 북 기준 좌표를 "드론 정면 기준"으로 회전. 진행방향 거리 fwd(앞)·side(옆)도 함께 산출.</figcaption>
|
||
</figure>
|
||
|
||
<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><span class="co">// toCameraCoords (301–317)</span></span>
|
||
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a>cc<span class="op">.</span><span class="at">fwd</span> <span class="op">=</span> relEnu[<span class="dv">0</span>]<span class="op">*</span>sy <span class="op">+</span> relEnu[<span class="dv">1</span>]<span class="op">*</span>cy<span class="op">;</span> <span class="co">// +면 앞쪽</span></span>
|
||
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a>cc<span class="op">.</span><span class="at">side</span> <span class="op">=</span> relEnu[<span class="dv">0</span>]<span class="op">*</span>cy <span class="op">-</span> relEnu[<span class="dv">1</span>]<span class="op">*</span>sy<span class="op">;</span> <span class="co">// +면 오른쪽 → "앞은 멀리, 옆은 가깝게" 비등방 필터 근거</span></span></code></pre></div>
|
||
<hr />
|
||
<h3 id="4-3-3d를-납작한-사진으로-핀홀-카메라-투영">4-3. 3D를 납작한 사진으로 (핀홀 카메라 투영)</h3>
|
||
<p>핵심 한 줄: <strong>깊이(Zc)로 나눈다 → 멀수록 화면 가운데로 작게.</strong> (바늘구멍 사진기 원리)</p>
|
||
<figure class="fig">
|
||
<svg viewBox="0 0 680 210" role="img" aria-label="핀홀 카메라">
|
||
<!-- pinhole -->
|
||
<rect x="330" y="55" width="16" height="100" fill="#475569"></rect>
|
||
<circle cx="338" cy="105" r="6" fill="#fffdf8" stroke="#475569"></circle>
|
||
<text x="338" y="172" text-anchor="middle" font-size="10" fill="#475569">구멍(렌즈)</text>
|
||
<!-- near big object -->
|
||
<line x1="120" y1="55" x2="120" y2="155" stroke="#16a34a" stroke-width="6"></line>
|
||
<text x="120" y="44" text-anchor="middle" font-size="10" fill="#14532d">가까운 🌉</text>
|
||
<!-- far small object -->
|
||
<line x1="40" y1="85" x2="40" y2="125" stroke="#22c55e" stroke-width="6"></line>
|
||
<text x="40" y="74" text-anchor="middle" font-size="10" fill="#15803d">먼 🌉</text>
|
||
<!-- rays through pinhole to screen -->
|
||
<line x1="120" y1="55" x2="338" y2="105" stroke="#16a34a" stroke-width="1" stroke-dasharray="3 3"></line>
|
||
<line x1="120" y1="155" x2="338" y2="105" stroke="#16a34a" stroke-width="1" stroke-dasharray="3 3"></line>
|
||
<line x1="338" y1="105" x2="470" y2="135" stroke="#16a34a" stroke-width="1" stroke-dasharray="3 3"></line>
|
||
<line x1="338" y1="105" x2="470" y2="75" stroke="#16a34a" stroke-width="1" stroke-dasharray="3 3"></line>
|
||
<!-- screen -->
|
||
<rect x="468" y="50" width="150" height="110" rx="6" fill="#0b1020" stroke="#334155"></rect>
|
||
<text x="543" y="44" text-anchor="middle" font-size="10" fill="#555">화면(0~1)</text>
|
||
<line x1="495" y1="75" x2="495" y2="135" stroke="#4ade80" stroke-width="5"></line>
|
||
<text x="555" y="110" font-size="10" fill="#86efac">가까운 게 크게 맺힘</text>
|
||
<text x="338" y="200" text-anchor="middle" font-size="11" fill="#555">거리(Zc)가 클수록 → 화면에 작게</text>
|
||
</svg>
|
||
<figcaption>그림 7. pxRaw = 0.5 + (Xc/Zc)·(f/sensorW). 0.5=정중앙, f/sensor=화각.</figcaption>
|
||
</figure>
|
||
|
||
<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="co">// pixelFromCamera (126–137)</span></span>
|
||
<span id="cb3-2"><a href="#cb3-2" 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> sW)<span class="op">;</span> <span class="co">// 가로 0~1</span></span>
|
||
<span id="cb3-3"><a href="#cb3-3" 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> sH)<span class="op">;</span> <span class="co">// 세로 0~1</span></span></code></pre></div>
|
||
<hr />
|
||
<h3 id="4-4-높이-기준-통일-지오이드-보정--화면-정렬">4-4. 높이 기준 통일 (지오이드 보정) + 화면 정렬</h3>
|
||
<p><strong>두 개의 '0층' 문제</strong>: 지도 높이(정표고·해발)와 GPS 높이(타원체고)는 기준이 달라 대전 기준 약 <strong>25.8m</strong> 차이. 안 맞추면 라벨이 위아래로 어긋납니다.</p>
|
||
<figure class="fig">
|
||
<svg viewBox="0 0 680 180" role="img" aria-label="지오이드 보정">
|
||
<defs><marker id="m44a" markerWidth="9" markerHeight="9" refX="3" refY="3" orient="auto"><path d="M0,0 L6,3 L0,6 Z" fill="#dc2626"></path></marker><marker id="m44b" markerWidth="9" markerHeight="9" refX="3" refY="3" orient="auto"><path d="M6,0 L0,3 L6,6 Z" fill="#dc2626"></path></marker></defs>
|
||
<line x1="60" y1="50" x2="500" y2="50" stroke="#2563eb" stroke-width="2.5" stroke-dasharray="7 4"></line>
|
||
<text x="510" y="54" font-size="12" fill="#1e3a8a">타원체고(GPS) 0</text>
|
||
<line x1="60" y1="110" x2="500" y2="110" stroke="#0ea5e9" stroke-width="2.5" stroke-dasharray="7 4"></line>
|
||
<text x="510" y="114" font-size="12" fill="#0369a1">정표고(해발) 0</text>
|
||
<line x1="150" y1="51" x2="150" y2="109" stroke="#dc2626" stroke-width="1.6" marker-start="url(#m44a)" marker-end="url(#m44b)"></line>
|
||
<text x="160" y="84" font-size="13" font-weight="bold" fill="#dc2626">≈ 25.8m (geoidOffset)</text>
|
||
<text x="160" y="100" font-size="10" fill="#b91c1c">지도 높이에 더해 GPS 기준으로 통일</text>
|
||
<text x="280" y="150" text-anchor="middle" font-size="11" fill="#555">기준을 맞춰야 라벨이 제 높이에 붙는다</text>
|
||
</svg>
|
||
<figcaption>그림 8. 지도 표고 + geoidOffset → GPS와 같은 기준. 마지막에 object-fit:cover 잘림을 반영해 0~1 → 실제 px.</figcaption>
|
||
</figure>
|
||
|
||
<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><span class="fu">geoToEnu</span>(lat<span class="op">,</span> lon<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="op">...</span>)<span class="op">;</span> <span class="co">// 높이 기준 통일</span></span>
|
||
<span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a><span class="co">// 이후 coverRef(StationOverlay 921–930)로 정규(0~1) → 실제 화면 px 정렬</span></span></code></pre></div>
|
||
<p>➡ 4-1 ~ 4-4를 거치면 <strong>POI가 영상 속 진짜 그 자리에</strong> 찍힙니다.</p>
|
||
<hr />
|
||
<h2 id="5-핵심-②-시간-동기화--영상-시각--드론-프레임">5. 핵심 ② 시간 동기화 — 영상 시각 ↔︎ 드론 프레임</h2>
|
||
<p>좌표가 맞아도 <strong>"지금 영상 시각의 드론 위치"</strong> 를 못 집으면 라벨이 엉뚱한 데 뜹니다.</p>
|
||
<p><strong>문제</strong>: 브라우저 <code>video.currentTime</code>은 ~250ms 간격으로만 갱신 → 커서·라벨이 뚝뚝 끊김. <strong>해결</strong>: <code>smoothTimeRef</code> — 벽시계로 <strong>60fps 단조 보간</strong>.</p>
|
||
<figure class="fig">
|
||
<svg viewBox="0 0 680 190" role="img" aria-label="시간 보간">
|
||
<!-- raw -->
|
||
<text x="40" y="42" font-size="12" font-weight="bold" fill="#b91c1c">실제 currentTime (250ms 띄엄띄엄)</text>
|
||
<line x1="50" y1="60" x2="630" y2="60" stroke="#e5e7eb" stroke-width="2"></line>
|
||
<g fill="#dc2626"><circle cx="80" cy="60" r="6"></circle><circle cx="220" cy="60" r="6"></circle><circle cx="360" cy="60" r="6"></circle><circle cx="500" cy="60" r="6"></circle><circle cx="620" cy="60" r="6"></circle></g>
|
||
<text x="340" y="84" text-anchor="middle" font-size="11" fill="#b91c1c">→ 라벨/커서가 뚝뚝 끊김</text>
|
||
<!-- smooth -->
|
||
<text x="40" y="124" font-size="12" font-weight="bold" fill="#166534">smoothTimeRef (매 프레임 채움 · 60fps)</text>
|
||
<line x1="50" y1="142" x2="630" y2="142" stroke="#e5e7eb" stroke-width="2"></line>
|
||
<g fill="#16a34a"><circle cx="80" cy="142" r="4"></circle><circle cx="110" cy="142" r="4"></circle><circle cx="140" cy="142" r="4"></circle><circle cx="170" cy="142" r="4"></circle><circle cx="200" cy="142" r="4"></circle><circle cx="230" cy="142" r="4"></circle><circle cx="260" cy="142" r="4"></circle><circle cx="290" cy="142" r="4"></circle><circle cx="320" cy="142" r="4"></circle><circle cx="350" cy="142" r="4"></circle><circle cx="380" cy="142" r="4"></circle><circle cx="410" cy="142" r="4"></circle><circle cx="440" cy="142" r="4"></circle><circle cx="470" cy="142" r="4"></circle><circle cx="500" cy="142" r="4"></circle><circle cx="530" cy="142" r="4"></circle><circle cx="560" cy="142" r="4"></circle><circle cx="590" cy="142" r="4"></circle><circle cx="620" cy="142" r="4"></circle></g>
|
||
<text x="340" y="166" text-anchor="middle" font-size="11" fill="#166534">media + 경과시간×배속 · 0.3s 이상 벌어지면 재동기화</text>
|
||
</svg>
|
||
<figcaption>그림 9. 띄엄띄엄한 실제 시각을 60fps로 메워 부드럽게. 이 t로 드론 pose를 보간.</figcaption>
|
||
</figure>
|
||
|
||
<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="co">// VideoPlayer.tsx 62–117</span></span>
|
||
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a>let est <span class="op">=</span> a<span class="op">.</span><span class="at">media</span> <span class="op">+</span> ((performance<span class="op">.</span><span class="fu">now</span>() <span class="op">-</span> a<span class="op">.</span><span class="at">wall</span>)<span class="op">/</span><span class="dv">1000</span>) <span class="op">*</span> rate<span class="op">;</span></span>
|
||
<span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a><span class="fu">if</span> (real <span class="op">-</span> est <span class="op">></span> <span class="fl">0.3</span>) est <span class="op">=</span> real<span class="op">;</span> <span class="co">// 재동기화</span></span>
|
||
<span id="cb5-4"><a href="#cb5-4" aria-hidden="true" tabindex="-1"></a>smoothTimeRef<span class="op">.</span><span class="at">current</span> <span class="op">=</span> t<span class="op">;</span> <span class="co">// ref로 노출 → 리렌더 없이 매 프레임 읽음</span></span></code></pre></div>
|
||
<hr />
|
||
<h2 id="6-핵심-③-라벨-안정화--떨림튐-제거">6. 핵심 ③ 라벨 안정화 — 떨림·튐 제거</h2>
|
||
<p>드론은 미세하게 흔들립니다. 그대로 투영하면 라벨이 부르르 떨립니다. → <strong>One Euro 방식 속도적응 평활 + 이상치 거부.</strong></p>
|
||
<figure class="fig">
|
||
<svg viewBox="0 0 680 220" role="img" aria-label="One Euro 평활">
|
||
<!-- case 1 -->
|
||
<rect x="20" y="20" width="210" height="180" rx="10" fill="#ecfdf5" stroke="#16a34a"></rect>
|
||
<text x="125" y="42" text-anchor="middle" font-size="12" font-weight="bold" fill="#14532d">떨림(왕복)</text>
|
||
<polyline points="40,110 60,95 80,118 100,92 120,116 140,96 160,114 180,98 200,110" fill="none" stroke="#dc2626" stroke-width="1.5"></polyline>
|
||
<line x1="40" y1="150" x2="210" y2="150" stroke="#16a34a" stroke-width="3"></line>
|
||
<text x="125" y="172" text-anchor="middle" font-size="10.5" fill="#166534">평활속도≈0 → 강하게 평활</text>
|
||
<text x="125" y="188" text-anchor="middle" font-size="10.5" fill="#166534">(안정)</text>
|
||
<!-- case 2 -->
|
||
<rect x="240" y="20" width="200" height="180" rx="10" fill="#eff6ff" stroke="#2563eb"></rect>
|
||
<text x="340" y="42" text-anchor="middle" font-size="12" font-weight="bold" fill="#1e3a8a">실제 이동</text>
|
||
<polyline points="260,150 290,130 320,110 350,90 380,70 410,55" fill="none" stroke="#2563eb" stroke-width="2.5"></polyline>
|
||
<text x="340" y="172" text-anchor="middle" font-size="10.5" fill="#1e40af">방향 일관 → 즉시 추종</text>
|
||
<text x="340" y="188" text-anchor="middle" font-size="10.5" fill="#1e40af">(지연 없음)</text>
|
||
<!-- case 3 -->
|
||
<rect x="450" y="20" width="210" height="180" rx="10" fill="#fff7ed" stroke="#d97706"></rect>
|
||
<text x="555" y="42" text-anchor="middle" font-size="12" font-weight="bold" fill="#9a3412">갑작스런 점프</text>
|
||
<polyline points="470,150 500,148 530,150" fill="none" stroke="#d97706" stroke-width="2"></polyline>
|
||
<circle cx="610" cy="80" r="6" fill="#dc2626"></circle>
|
||
<line x1="530" y1="150" x2="600" y2="86" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="4 3"></line>
|
||
<text x="612" y="76" font-size="14" fill="#dc2626">✗</text>
|
||
<text x="555" y="180" text-anchor="middle" font-size="10.5" fill="#9a3412">8프레임까지 무시 후 수용</text>
|
||
<text x="555" y="195" text-anchor="middle" font-size="10.5" fill="#9a3412">(시크는 진짜 → 결국 수용)</text>
|
||
</svg>
|
||
<figcaption>그림 10. 떨림은 죽이고, 진짜 이동은 즉시 따라가고, 튐(점프)은 무시. 세 동작을 한 필터로.</figcaption>
|
||
</figure>
|
||
|
||
<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">// StationOverlay.tsx 48–61</span></span>
|
||
<span id="cb6-2"><a href="#cb6-2" 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_REJECT_FRAMES) <span class="co">// 0.12 초과 점프 = 이상치</span></span>
|
||
<span id="cb6-3"><a href="#cb6-3" aria-hidden="true" tabindex="-1"></a> return { <span class="op">...</span>prev<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="co">// 위치 유지</span></span>
|
||
<span id="cb6-4"><a href="#cb6-4" 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="cb6-5"><a href="#cb6-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 class="co">// 속도 클수록 빠르게 추종</span></span></code></pre></div>
|
||
<hr />
|
||
<h2 id="7-핵심-④-프레임-기반--측점스테이션-기반">7. 핵심 ④ 프레임 기반 → 측점(스테이션) 기반</h2>
|
||
<h3 id="7-1-왜-바꿨나--시간축의-한계">7-1. 왜 바꿨나? — 시간축의 한계</h3>
|
||
<p>철도 현장은 <strong>위치(측점)</strong> 로 말합니다: "157K970 지점의 교량". 그런데 시간축은 <strong>드론이 호버(정지)</strong> 하면 무너집니다.</p>
|
||
<figure class="fig">
|
||
<svg viewBox="0 0 680 180" role="img" aria-label="시간축 한계">
|
||
<text x="120" y="32" font-size="12" font-weight="bold" fill="#b91c1c">시간축 (균등)</text>
|
||
<line x1="40" y1="60" x2="500" y2="60" stroke="#cbd5e1" stroke-width="3"></line>
|
||
<g fill="#64748b"><circle cx="40" cy="60" r="4"></circle><circle cx="132" cy="60" r="4"></circle><circle cx="224" cy="60" r="4"></circle><circle cx="316" cy="60" r="4"></circle><circle cx="408" cy="60" r="4"></circle><circle cx="500" cy="60" r="4"></circle></g>
|
||
<g font-size="10" fill="#64748b"><text x="40" y="78">0s</text><text x="128" y="78">1s</text><text x="220" y="78">2s</text><text x="312" y="78">3s</text><text x="404" y="78">4s</text><text x="496" y="78">5s</text></g>
|
||
<!-- hover -->
|
||
<rect x="224" y="100" width="120" height="50" rx="8" fill="#fee2e2" stroke="#dc2626"></rect>
|
||
<text x="284" y="120" text-anchor="middle" font-size="11" fill="#b91c1c">드론 호버(정지)</text>
|
||
<text x="284" y="138" text-anchor="middle" font-size="10" fill="#b91c1c">시간은 가는데 위치는 안 변함</text>
|
||
<text x="560" y="125" font-size="11" fill="#b91c1c">→ 시간축에선</text>
|
||
<text x="560" y="142" font-size="11" fill="#b91c1c">구분 불가 ✗</text>
|
||
</svg>
|
||
<figcaption>그림 11. 시간 균등축에선 "공중 대기(호버)" 구간을 표현할 수 없다.</figcaption>
|
||
</figure>
|
||
|
||
<p>➡ 그래서 <strong>측점(공간)축</strong>으로 재해석: 드론이 호버하면 측점도 멈춰 있어야 자연스럽다.</p>
|
||
<hr />
|
||
<h3 id="7-2-구현-①--gps를-측점값으로-투영">7-2. 구현 ① — GPS를 측점값으로 투영</h3>
|
||
<p>드론 GPS를 측점 폴리라인(선로)에 <strong>수직으로 내려</strong> "측점값(km)"과 "선로 이격(m)"을 구합니다.</p>
|
||
<figure class="fig">
|
||
<svg viewBox="0 0 680 190" role="img" aria-label="GPS를 측점선에 투영">
|
||
<defs><marker id="m72" markerWidth="9" markerHeight="9" refX="6" refY="3" orient="auto"><path d="M0,0 L6,3 L0,6 Z" fill="#f59e0b"></path></marker></defs>
|
||
<!-- chain line -->
|
||
<polyline points="60,130 200,122 360,118 520,124 630,130" fill="none" stroke="#0891b2" stroke-width="5"></polyline>
|
||
<g fill="#155e75" font-size="10">
|
||
<circle cx="60" cy="130" r="5" fill="#0891b2"></circle><text x="40" y="152">157K900</text>
|
||
<circle cx="360" cy="118" r="5" fill="#0891b2"></circle><text x="335" y="108">158K000</text></g>
|
||
<text x="70" y="118" font-size="11" fill="#155e75">측점 폴리라인(선로)</text>
|
||
<!-- drone -->
|
||
<circle cx="280" cy="55" r="9" fill="#7c3aed"></circle><text x="295" y="52" font-size="12" font-weight="bold" fill="#5b21b6">🚁 드론 GPS</text>
|
||
<!-- perpendicular -->
|
||
<line x1="280" y1="64" x2="280" y2="118" stroke="#f59e0b" stroke-width="2" stroke-dasharray="5 4" marker-end="url(#m72)"></line>
|
||
<circle cx="280" cy="120" r="6" fill="#b45309"></circle>
|
||
<text x="290" y="146" font-size="12" font-weight="bold" fill="#b45309">km=157K958, 이격 4.2m</text>
|
||
<text x="200" y="92" font-size="10" fill="#9a3412">projectToChain (수직 내림)</text>
|
||
</svg>
|
||
<figcaption>그림 12. 측점명 "157K970" → 157970m 환산으로 선로를 만들고, GPS를 수직 투영해 측점값·이격 산출.</figcaption>
|
||
</figure>
|
||
|
||
<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="co">// chainage.ts projectToChain (35–50)</span></span>
|
||
<span id="cb7-2"><a href="#cb7-2" aria-hidden="true" tabindex="-1"></a>const t <span class="op">=</span> <span class="fu">clamp</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 class="dv">0</span><span class="op">,</span> <span class="dv">1</span>)<span class="op">;</span> <span class="co">// 선분 위 투영 비율</span></span>
|
||
<span id="cb7-3"><a href="#cb7-3" aria-hidden="true" tabindex="-1"></a>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">// 보간 측점값</span></span></code></pre></div>
|
||
<hr />
|
||
<h3 id="7-3-구현-②--이동거리축-핵심-아이디어">7-3. 구현 ② — 이동거리축 (핵심 아이디어)</h3>
|
||
<p>시간축 대신 <strong>드론이 실제로 이동한 거리</strong>를 축으로 씁니다. 측점값 변화량 |Δ|을 매 프레임 누적.</p>
|
||
<figure class="fig">
|
||
<svg viewBox="0 0 680 200" role="img" aria-label="이동거리축">
|
||
<!-- timeline of values -->
|
||
<text x="40" y="32" font-size="11" fill="#334155">측점값(평활): 920 → 930 → 940 → <tspan fill="#b91c1c">940 940 940</tspan> → 950 → 960</text>
|
||
<!-- accumulation bar -->
|
||
<text x="40" y="72" font-size="11" font-weight="bold" fill="#155e75">누적 이동거리 = 축 위치</text>
|
||
<rect x="40" y="84" width="600" height="26" rx="13" fill="#e5e7eb"></rect>
|
||
<!-- run 1 -->
|
||
<rect x="40" y="84" width="180" height="26" rx="13" fill="#67e8f9"></rect>
|
||
<!-- hover (no growth) -->
|
||
<rect x="220" y="84" width="0" height="26" fill="#fca5a5"></rect>
|
||
<!-- run 2 -->
|
||
<rect x="220" y="84" width="180" height="26" fill="#22d3ee"></rect>
|
||
<circle cx="220" cy="97" r="11" fill="#0891b2"></circle>
|
||
<text x="220" y="101" text-anchor="middle" font-size="10" fill="#fff">⏸</text>
|
||
<text x="130" y="135" text-anchor="middle" font-size="10" fill="#155e75">정상 주행 →→→</text>
|
||
<text x="232" y="135" font-size="10" fill="#b91c1c">호버: 누적 멈춤 → 커서 정지!</text>
|
||
<text x="540" y="135" text-anchor="middle" font-size="10" fill="#155e75">→→→ 주행</text>
|
||
<text x="340" y="172" text-anchor="middle" font-size="11" fill="#555">호버 구간에서 커서가 멈춰 "공중 대기"가 한눈에 보인다</text>
|
||
</svg>
|
||
<figcaption>그림 13. |Δ측점값|을 누적해 0~1로 정규화 → 축 위치. 호버는 누적이 멈추므로 커서도 정지.</figcaption>
|
||
</figure>
|
||
|
||
<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><span class="co">// StationBar.tsx 233–274</span></span>
|
||
<span id="cb8-2"><a href="#cb8-2" aria-hidden="true" tabindex="-1"></a>sm[i] <span class="op">=</span> (pre[hi]<span class="op">-</span>pre[lo])<span class="op">/</span>(hi<span class="op">-</span>lo)<span class="op">;</span> <span class="co">// 측점값 ±8프레임 평활</span></span>
|
||
<span id="cb8-3"><a href="#cb8-3" aria-hidden="true" tabindex="-1"></a><span class="fu">if</span> (i<span class="op">></span><span class="dv">0</span>) cum <span class="op">+=</span> <span class="bu">Math</span><span class="op">.</span><span class="fu">abs</span>(sm[i]<span class="op">-</span>sm[i<span class="op">-</span><span class="dv">1</span>])<span class="op">;</span> <span class="co">// |Δ| 누적 = 실제 이동거리</span></span>
|
||
<span id="cb8-4"><a href="#cb8-4" aria-hidden="true" tabindex="-1"></a>frac[i] <span class="op">=</span> cum <span class="op">/</span> total<span class="op">;</span> <span class="co">// 0~1 정규화 → 축 위치</span></span></code></pre></div>
|
||
<hr />
|
||
<h3 id="7-4-측점-기반의-결과--하단-측점바">7-4. 측점 기반의 결과 — 하단 측점바</h3>
|
||
<figure class="fig">
|
||
<svg viewBox="0 0 680 170" role="img" aria-label="하단 측점바">
|
||
<rect x="20" y="30" width="640" height="110" rx="10" fill="#0f172a" stroke="#334155"></rect>
|
||
<!-- track -->
|
||
<line x1="60" y1="95" x2="520" y2="95" stroke="#22d3ee" stroke-width="5"></line>
|
||
<line x1="520" y1="95" x2="620" y2="95" stroke="#475569" stroke-width="5" stroke-dasharray="2 4"></line>
|
||
<!-- markers -->
|
||
<g font-size="9" fill="#e2e8f0" text-anchor="middle">
|
||
<circle cx="60" cy="95" r="7" fill="#fbbf24"></circle><text x="60" y="72">🚉역</text>
|
||
<circle cx="170" cy="95" r="6" fill="#f59e0b"></circle><text x="170" y="72">🌉교량</text>
|
||
<circle cx="300" cy="95" r="6" fill="#38bdf8"></circle><text x="300" y="72">🚇터널</text>
|
||
<circle cx="460" cy="95" r="6" fill="#f59e0b"></circle><text x="460" y="72">🌉구교</text>
|
||
<circle cx="620" cy="95" r="7" fill="none" stroke="#94a3b8" stroke-width="2"></circle><text x="620" y="72">◌종점</text></g>
|
||
<!-- cursor -->
|
||
<polygon points="220,80 212,68 228,68" fill="#fff"></polygon>
|
||
<line x1="220" y1="80" x2="220" y2="110" stroke="#fff" stroke-width="2"></line>
|
||
<text x="220" y="128" text-anchor="middle" font-size="10" fill="#fff">현재 위치 커서 (60fps)</text>
|
||
<text x="572" y="118" font-size="9" fill="#94a3b8">회색=미도착</text>
|
||
</svg>
|
||
<figcaption>그림 14. 측점·구조물을 공간축에 배치. 영상별 FPS 자동산출로 배지 정밀화 · 종점 미도착은 회색 + 빈 링.</figcaption>
|
||
</figure>
|
||
|
||
<hr />
|
||
<h3 id="7-5-프레임--측점--한-장-비교">7-5. 프레임 ↔︎ 측점 — 한 장 비교</h3>
|
||
<table>
|
||
<thead>
|
||
<tr class="header">
|
||
<th>구분</th>
|
||
<th>프레임 기반</th>
|
||
<th>측점 기반 (현재)</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr class="odd">
|
||
<td>축</td>
|
||
<td>시간(초)</td>
|
||
<td>실제 이동거리/측점(km)</td>
|
||
</tr>
|
||
<tr class="even">
|
||
<td>호버</td>
|
||
<td>구분 불가</td>
|
||
<td>커서 정지로 가시화</td>
|
||
</tr>
|
||
<tr class="odd">
|
||
<td>현장 용어</td>
|
||
<td>"3분 20초"</td>
|
||
<td>"157K970 교량"</td>
|
||
</tr>
|
||
<tr class="even">
|
||
<td>위치 질의</td>
|
||
<td>어려움</td>
|
||
<td>GPS→측점 투영으로 즉답</td>
|
||
</tr>
|
||
<tr class="odd">
|
||
<td>동기화</td>
|
||
<td>currentTime</td>
|
||
<td>smoothTimeRef + 측점투영</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
<p>➡ <strong>시간축 영상을 공간축(측점)으로 재해석</strong>한 것이 이 프로그램의 정체성.</p>
|
||
<hr />
|
||
<h2 id="8-정확도-보정-도구">8. 정확도 보정 도구</h2>
|
||
<p>투영은 카메라 파라미터·표고 가정에 민감 → <strong>사용자가 미세보정</strong>할 수 있게 함. 핵심은 투영의 <strong>역방향</strong>(화면→세계)을 푸는 것.</p>
|
||
<figure class="fig">
|
||
<svg viewBox="0 0 680 210" role="img" aria-label="보정 도구">
|
||
<defs><marker id="m8" markerWidth="10" markerHeight="10" refX="7" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7 Z" fill="#7c3aed"></path></marker></defs>
|
||
<!-- screen -->
|
||
<rect x="40" y="40" width="220" height="130" rx="6" fill="#0b1020" stroke="#334155"></rect>
|
||
<text x="150" y="32" text-anchor="middle" font-size="11" fill="#555">영상 화면</text>
|
||
<circle cx="120" cy="80" r="6" fill="#f59e0b"></circle>
|
||
<text x="60" y="105" font-size="10" fill="#fde68a">라벨을 끌어 제자리로</text>
|
||
<!-- ground -->
|
||
<line x1="320" y1="175" x2="650" y2="175" stroke="#8b5e34" stroke-width="3"></line>
|
||
<text x="320" y="193" font-size="10" fill="#7c5a3a">실제 땅</text>
|
||
<line x1="126" y1="82" x2="540" y2="170" stroke="#7c3aed" stroke-width="2" stroke-dasharray="5 4" marker-end="url(#m8)"></line>
|
||
<circle cx="540" cy="170" r="6" fill="#7c3aed"></circle>
|
||
<text x="470" y="160" font-size="11" font-weight="bold" fill="#6d28d9">위경도 역산</text>
|
||
<text x="300" y="60" font-size="11" fill="#6d28d9">화면점 → 광선 → 지면 교차 → 실제 위치</text>
|
||
<text x="300" y="80" font-size="10" fill="#6d28d9">groundPointFromPixel</text>
|
||
</svg>
|
||
<figcaption>그림 15. "끌어다 맞추기" = 역투영. 끈 화면점을 광선으로 쏴 지면과 만나는 실제 좌표를 복원.</figcaption>
|
||
</figure>
|
||
|
||
<table>
|
||
<thead>
|
||
<tr class="header">
|
||
<th>도구</th>
|
||
<th>동작</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr class="odd">
|
||
<td>① <strong>DEM 자동표고</strong></td>
|
||
<td>모든 POI를 실제 지형고도로 (서버 <code>/api/elevation</code>, SRTM 30m)</td>
|
||
</tr>
|
||
<tr class="even">
|
||
<td>② <strong>드래그 편집</strong></td>
|
||
<td>라벨을 끌면 <code>groundPointFromPixel</code>로 위경도 역산</td>
|
||
</tr>
|
||
<tr class="odd">
|
||
<td>③ <strong>세로화각 보정</strong></td>
|
||
<td>POI를 실제 위치로 끌면 <code>sensorH</code>만 역산 자동보정</td>
|
||
</tr>
|
||
<tr class="even">
|
||
<td>④ <strong>보정값 저장</strong></td>
|
||
<td><code>poi_overrides.json</code> 내보내기/가져오기</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
<hr />
|
||
<h2 id="9-정리--무엇을-어떻게">9. 정리 — 무엇을, 어떻게</h2>
|
||
<figure class="fig">
|
||
<svg viewBox="0 0 680 260" role="img" aria-label="전체 정리">
|
||
<defs><marker id="m9" markerWidth="10" markerHeight="10" refX="7" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7 Z" fill="#64748b"></path></marker></defs>
|
||
<!-- inputs -->
|
||
<g text-anchor="middle">
|
||
<rect x="20" y="40" width="120" height="34" rx="8" fill="#f5f3ff" stroke="#7c3aed"></rect><text x="80" y="62" font-size="11" fill="#5b21b6">드론 GPS</text>
|
||
<rect x="20" y="100" width="120" height="34" rx="8" fill="#eff6ff" stroke="#2563eb"></rect><text x="80" y="122" font-size="11" fill="#1e3a8a">영상</text>
|
||
<rect x="20" y="160" width="120" height="34" rx="8" fill="#ecfdf5" stroke="#16a34a"></rect><text x="80" y="182" font-size="11" fill="#14532d">POI 좌표</text>
|
||
</g>
|
||
<!-- stages -->
|
||
<g text-anchor="middle">
|
||
<rect x="200" y="40" width="130" height="60" rx="8" fill="#ecfdf5" stroke="#16a34a"></rect><text x="265" y="64" font-size="11" font-weight="bold" fill="#14532d">🟩 시간 동기화</text><text x="265" y="82" font-size="9" fill="#166534">smoothTimeRef</text><text x="265" y="95" font-size="9" fill="#166534">60fps 보간</text>
|
||
<rect x="200" y="110" width="130" height="74" rx="8" fill="#eff6ff" stroke="#2563eb"></rect><text x="265" y="132" font-size="11" font-weight="bold" fill="#1e3a8a">🟦 좌표 투영</text><text x="265" y="150" font-size="9" fill="#1e40af">ENU→카메라</text><text x="265" y="163" font-size="9" fill="#1e40af">→핀홀→cover</text><text x="265" y="176" font-size="9" fill="#1e40af">+geoid 보정</text>
|
||
<rect x="380" y="75" width="120" height="60" rx="8" fill="#fff7ed" stroke="#d97706"></rect><text x="440" y="99" font-size="11" font-weight="bold" fill="#9a3412">🟧 평활</text><text x="440" y="117" font-size="9" fill="#9a3412">One Euro +</text><text x="440" y="129" font-size="9" fill="#9a3412">이상치 거부</text>
|
||
<rect x="540" y="75" width="120" height="60" rx="8" fill="#fff" stroke="#23272e" stroke-width="1.5"></rect><text x="600" y="99" font-size="11" font-weight="bold" fill="#23272e">영상 위</text><text x="600" y="117" font-size="11" font-weight="bold" fill="#23272e">정확한 라벨✨</text>
|
||
<rect x="380" y="170" width="280" height="40" rx="8" fill="#ecfeff" stroke="#0891b2"></rect><text x="520" y="188" font-size="11" font-weight="bold" fill="#155e75">🟦 GPS→측점 투영 → 이동거리축</text><text x="520" y="202" font-size="9" fill="#155e75">→ 측점 기반 측점바 (호버까지 표현)</text>
|
||
</g>
|
||
<!-- arrows -->
|
||
<line x1="140" y1="70" x2="198" y2="70" stroke="#64748b" stroke-width="1.4" marker-end="url(#m9)"></line>
|
||
<line x1="140" y1="117" x2="198" y2="135" stroke="#64748b" stroke-width="1.4" marker-end="url(#m9)"></line>
|
||
<line x1="140" y1="177" x2="198" y2="155" stroke="#64748b" stroke-width="1.4" marker-end="url(#m9)"></line>
|
||
<line x1="330" y1="120" x2="378" y2="105" stroke="#64748b" stroke-width="1.4" marker-end="url(#m9)"></line>
|
||
<line x1="500" y1="105" x2="538" y2="105" stroke="#64748b" stroke-width="1.4" marker-end="url(#m9)"></line>
|
||
<line x1="265" y1="184" x2="265" y2="195" stroke="#0891b2" stroke-width="1.4"></line>
|
||
<line x1="265" y1="195" x2="378" y2="190" stroke="#0891b2" stroke-width="1.4" marker-end="url(#m9)"></line>
|
||
</svg>
|
||
<figcaption>그림 16. 3대 정합(공간·시간·안정) + 패러다임 전환(프레임→측점).</figcaption>
|
||
</figure>
|
||
|
||
<p><strong>3대 정합 기술</strong></p>
|
||
<ol type="1">
|
||
<li><strong>🟦 공간</strong>: 4단계 투영(ENU→카메라회전→핀홀→cover) + 지오이드 보정 + 역투영 보정</li>
|
||
<li><strong>🟩 시간</strong>: smoothTimeRef 60fps 단조보간으로 영상시각↔︎드론프레임 정합</li>
|
||
<li><strong>🟧 안정</strong>: One Euro 속도적응 평활 + 이상치 거부</li>
|
||
</ol>
|
||
<p><strong>패러다임 전환</strong></p>
|
||
<ul>
|
||
<li>프레임(시간) 기반 → <strong>측점(공간) 기반</strong>: GPS를 선로에 투영해 측점값화, 이동거리축으로 호버까지 표현</li>
|
||
</ul>
|
||
<hr />
|
||
<h2 id="부록--핵심-소스-색인">부록 — 핵심 소스 색인</h2>
|
||
<table>
|
||
<thead>
|
||
<tr class="header">
|
||
<th>기술</th>
|
||
<th>파일</th>
|
||
<th>라인</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr class="odd">
|
||
<td>ENU 변환</td>
|
||
<td>geoProjection.ts</td>
|
||
<td>91–97</td>
|
||
</tr>
|
||
<tr class="even">
|
||
<td>카메라 좌표 + fwd/side</td>
|
||
<td>geoProjection.ts</td>
|
||
<td>301–317</td>
|
||
</tr>
|
||
<tr class="odd">
|
||
<td>핀홀 투영</td>
|
||
<td>geoProjection.ts</td>
|
||
<td>126–137</td>
|
||
</tr>
|
||
<tr class="even">
|
||
<td>지오이드 보정</td>
|
||
<td>geoProjection.ts</td>
|
||
<td>53–67, projectPoint</td>
|
||
</tr>
|
||
<tr class="odd">
|
||
<td>역투영(보정)</td>
|
||
<td>geoProjection.ts</td>
|
||
<td>149–251</td>
|
||
</tr>
|
||
<tr class="even">
|
||
<td>object-fit cover 정렬</td>
|
||
<td>StationOverlay.tsx</td>
|
||
<td>921–930</td>
|
||
</tr>
|
||
<tr class="odd">
|
||
<td>라벨 평활/이상치</td>
|
||
<td>StationOverlay.tsx</td>
|
||
<td>48–61</td>
|
||
</tr>
|
||
<tr class="even">
|
||
<td>RAF 60fps 렌더</td>
|
||
<td>StationOverlay.tsx</td>
|
||
<td>905–1198</td>
|
||
</tr>
|
||
<tr class="odd">
|
||
<td>smoothTimeRef</td>
|
||
<td>VideoPlayer.tsx</td>
|
||
<td>62–117</td>
|
||
</tr>
|
||
<tr class="even">
|
||
<td>측점 투영(chainage)</td>
|
||
<td>chainage.ts</td>
|
||
<td>10–64</td>
|
||
</tr>
|
||
<tr class="odd">
|
||
<td>이동거리축</td>
|
||
<td>StationBar.tsx</td>
|
||
<td>233–274</td>
|
||
</tr>
|
||
<tr class="even">
|
||
<td>종점역 미도착</td>
|
||
<td>StationBar.tsx</td>
|
||
<td>300–310, 574–586</td>
|
||
</tr>
|
||
<tr class="odd">
|
||
<td>DEM 표고</td>
|
||
<td>StationOverlay.tsx / elevation.ts</td>
|
||
<td>1384–1415 / 14–67</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
<blockquote>
|
||
<p>상세 구현은 <a href="구현상세_GhiVideo_기술-소스코드매칭.md">구현상세_GhiVideo_기술-소스코드매칭.md</a> 참조.</p>
|
||
</blockquote>
|
||
</body>
|
||
</html>
|