Files
sum-parts-test/docs/sum-parts-explained.html
nbrightandClaude Opus 5 e529c449b5 Add the paper study material and set the bare-earth goal
Six papers converted to Markdown with the local doc2md tool, figures extracted
and annotated. The tool's venv had a CPU-only torch, so marker-pdf silently ran
on CPU and stalled; swapping in 2.5.1+cu121 dropped a paper from "hung after six
minutes" to three. Gemini then described all 86 figures in place, below each
original caption.

The PDFs themselves are gitignored - 88 MB of public arXiv downloads that
convert_papers.sh regenerates. The .md and figures are tracked, because the
annotations took a separate pass and do not reproduce byte-for-byte.

sum-parts-explained.html gains two tabs:

- PointVector. Why representing a scalar feature as a rotated 3D vector buys
  anisotropic aggregation without attention's cost, and why the paper predicts
  two independent angles rather than a rotation matrix whose nine elements are
  interdependent.
- Bare Earth. Reframes the task as ground vs not-ground, and separates the five
  boundaries by their nature. Four of them are cuts; the slope boundary is the
  one that must NOT be cut, which is why "horizontal means ground" destroys road
  cut and fill. Notes that SUM Parts is flat Helsinki and cannot teach slopes at
  all, so that part needs a geometric filter rather than more training.

NEXT.md carries the goal forward: separate bare earth from the rest as OBJ
meshes, then reclassify the remainder. Removing the ground first is sound -
it is 24-40% of the points, and without it the remaining objects fall apart
into separate connected components instead of being joined through the floor.

The gap that blocks step 4 is named: mesh_to_ply.py samples points without
recording which face each came from, so there is no way back to the mesh yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 12:53:50 +09:00

966 lines
42 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<title>SUM Parts 해설</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans+KR:wght@300;400;500;600;700&display=swap">
<style>
/* Same palette family as the pipeline document: derived from the dataset's
own class colours (terrain / high_vegetation / water), desaturated so they
read as document colour rather than legend swatches. */
:root {
--ground: #F6F7F4;
--surface: #FFFFFF;
--surface-sunk: #EDEFEA;
--line: #D8DCD3;
--line-soft: #E6E9E1;
--ink: #1A1F1B;
--ink-soft: #4B534C;
--ink-faint: #7C857D;
--veg: #2F6F4E;
--terr: #8A5A2B;
--water: #2A6E7A;
--signal: #B4482D;
--veg-wash: #E7F0EA;
--water-wash: #E4EEF0;
--signal-wash: #F8E9E4;
--s0: .75rem; --s1: .8125rem; --s2: .9375rem;
--s3: 1.0625rem; --s4: 1.375rem; --s5: 2rem; --s6: 2.75rem;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--ground: #12150F;
--surface: #191D16;
--surface-sunk: #10130E;
--line: #2C3229;
--line-soft: #232821;
--ink: #E9ECE6;
--ink-soft: #AEB6AC;
--ink-faint: #7B8479;
--veg: #6FBF8E;
--terr: #C79A63;
--water: #6BB6C2;
--signal: #E08A6B;
--veg-wash: #1B2A20;
--water-wash: #172629;
--signal-wash: #2E1D17;
}
}
:root[data-theme="dark"] {
--ground: #12150F;
--surface: #191D16;
--surface-sunk: #10130E;
--line: #2C3229;
--line-soft: #232821;
--ink: #E9ECE6;
--ink-soft: #AEB6AC;
--ink-faint: #7B8479;
--veg: #6FBF8E;
--terr: #C79A63;
--water: #6BB6C2;
--signal: #E08A6B;
--veg-wash: #1B2A20;
--water-wash: #172629;
--signal-wash: #2E1D17;
}
* { box-sizing: border-box; }
[hidden] { display: none !important; }
body {
margin: 0;
background: var(--ground);
color: var(--ink);
font-family: "IBM Plex Sans KR", system-ui, sans-serif;
font-size: var(--s2);
line-height: 1.7;
-webkit-font-smoothing: antialiased;
}
.wrap { max-width: 46rem; margin: 0 auto; padding: 3.5rem 1.5rem 6rem; }
code, .mono { font-family: "IBM Plex Mono", ui-monospace, monospace; }
code { font-size: .88em; background: var(--surface-sunk); padding: .1em .35em; border-radius: 2px; }
.eyebrow {
font-family: "IBM Plex Mono", monospace;
font-size: var(--s0); letter-spacing: .14em; text-transform: uppercase;
color: var(--ink-faint); margin-bottom: 1rem;
}
h1 {
margin: 0 0 1rem;
font-size: clamp(1.9rem, 5vw, var(--s6));
font-weight: 600; letter-spacing: -.025em; line-height: 1.1;
text-wrap: balance;
}
.lede {
margin: 0 0 2rem;
font-size: var(--s3); font-weight: 300; color: var(--ink-soft);
max-width: 40ch; text-wrap: pretty;
}
/* ── tabs ─────────────────────────────────────────────────────────── */
.tabs {
position: sticky; top: 0; z-index: 10;
background: var(--ground);
border-bottom: 1px solid var(--line);
margin: 0 0 .25rem;
}
.tablist {
display: flex; gap: .125rem;
overflow-x: auto; scrollbar-width: none;
}
.tablist::-webkit-scrollbar { display: none; }
.tab {
appearance: none; background: none; border: 0;
border-bottom: 2px solid transparent;
font-family: inherit; font-size: var(--s1); font-weight: 500;
color: var(--ink-faint);
padding: .8rem .7rem .65rem;
white-space: nowrap; cursor: pointer;
transition: color .12s ease, border-color .12s ease;
}
.tab:hover { color: var(--ink-soft); }
.tab[aria-selected="true"] { color: var(--ink); border-bottom-color: var(--veg); }
.tab:focus-visible { outline: 2px solid var(--water); outline-offset: -3px; border-radius: 2px; }
.panel { animation: rise .18s ease-out; }
.panel:focus { outline: none; }
@keyframes rise { from { opacity: 0; transform: translateY(3px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) {
.panel { animation: none; }
.tab { transition: none; }
}
/* first heading in a panel sits right under the tab bar */
.panel > h2:first-child { margin-top: 1.75rem; }
h2 {
margin: 3rem 0 1rem;
font-size: var(--s4); font-weight: 600; letter-spacing: -.015em;
padding-bottom: .4rem; border-bottom: 1px solid var(--line);
}
h3 { margin: 1.75rem 0 .5rem; font-size: var(--s3); font-weight: 600; }
p { margin: 0 0 1rem; }
ul, ol { margin: 0 0 1rem; padding-left: 1.2rem; }
li { margin-bottom: .35rem; }
/* one-line definition blocks */
.def {
display: grid; grid-template-columns: 9rem 1fr; gap: .75rem 1rem;
background: var(--surface); border: 1px solid var(--line);
border-radius: 3px; padding: 1rem 1.125rem; margin: 0 0 1.25rem;
}
.def dt {
font-weight: 600; font-size: var(--s1);
color: var(--veg); align-self: start;
}
.def dd { margin: 0; font-size: var(--s1); color: var(--ink); }
@media (max-width: 30rem) { .def { grid-template-columns: 1fr; gap: .25rem; } .def dd { margin-bottom: .5rem; } }
.scroll { overflow-x: auto; margin: 0 0 1.25rem; border: 1px solid var(--line); border-radius: 3px; }
table { border-collapse: collapse; width: 100%; font-size: var(--s1); }
th, td { padding: .45rem .7rem; text-align: left; border-bottom: 1px solid var(--line-soft); }
thead th {
background: var(--surface-sunk);
font-family: "IBM Plex Mono", monospace; font-size: var(--s0);
font-weight: 500; letter-spacing: .05em; text-transform: uppercase;
color: var(--ink-faint); white-space: nowrap;
}
tbody tr:last-child td { border-bottom: none; }
td.num { font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap; }
td.key { font-family: "IBM Plex Mono", monospace; white-space: nowrap; color: var(--ink); }
.win { color: var(--veg); font-weight: 600; }
.bad { color: var(--signal); font-weight: 600; }
.note, .warn {
border-radius: 3px; padding: .875rem 1rem; margin: 1.25rem 0;
font-size: var(--s1);
}
.note { background: var(--water-wash); }
.warn { background: var(--signal-wash); }
.note strong, .warn strong { display: block; margin-bottom: .3rem; }
.warn strong { color: var(--signal); }
.note strong { color: var(--water); }
.note p:last-child, .warn p:last-child { margin-bottom: 0; }
.flow {
font-family: "IBM Plex Mono", monospace; font-size: var(--s1);
background: var(--surface-sunk); border-radius: 3px;
padding: .875rem 1rem; margin: 0 0 1.25rem;
overflow-x: auto; white-space: pre; line-height: 1.8;
}
.lineage { display: flex; flex-direction: column; gap: .5rem; margin: 0 0 1.25rem; }
.gen {
display: grid; grid-template-columns: 4.5rem 1fr auto; gap: .875rem;
align-items: baseline;
background: var(--surface); border: 1px solid var(--line);
border-left: 3px solid var(--accent, var(--line));
border-radius: 3px; padding: .7rem .9rem;
}
.gen .yr { font-family: "IBM Plex Mono", monospace; font-size: var(--s0); color: var(--ink-faint); }
.gen .nm { font-weight: 600; font-size: var(--s2); }
.gen .ds { grid-column: 2 / -1; font-size: var(--s1); color: var(--ink-soft); }
.gen .sc { font-family: "IBM Plex Mono", monospace; font-size: var(--s1); font-variant-numeric: tabular-nums; }
.caption { font-size: var(--s1); color: var(--ink-soft); }
footer {
margin-top: 3.5rem; padding-top: 1.25rem; border-top: 1px solid var(--line);
font-size: var(--s1); color: var(--ink-faint);
}
a { color: var(--water); }
</style>
<div class="wrap">
<div class="eyebrow">해설 · 2026-08-24</div>
<h1>SUM Parts 해설</h1>
<p class="lede">
도시 3D 메시를 부분 단위로 분할하는 벤치마크가 무엇이고,
우리 과제에 어떻게 쓰이는가.
</p>
<div class="tabs">
<div class="tablist" role="tablist" aria-label="문서 구역">
<button class="tab" id="t-overview" role="tab" data-key="overview" aria-controls="p-overview" aria-selected="true">개요</button>
<button class="tab" id="t-data" role="tab" data-key="data" aria-controls="p-data" aria-selected="false" tabindex="-1">데이터셋</button>
<button class="tab" id="t-cloud" role="tab" data-key="cloud" aria-controls="p-cloud" aria-selected="false" tabindex="-1">포인트 클라우드</button>
<button class="tab" id="t-models" role="tab" data-key="models" aria-controls="p-models" aria-selected="false" tabindex="-1">모델 계보</button>
<button class="tab" id="t-vector" role="tab" data-key="vector" aria-controls="p-vector" aria-selected="false" tabindex="-1">PointVector</button>
<button class="tab" id="t-bare" role="tab" data-key="bare" aria-controls="p-bare" aria-selected="false" tabindex="-1">Bare Earth</button>
<button class="tab" id="t-results" role="tab" data-key="results" aria-controls="p-results" aria-selected="false" tabindex="-1">결과</button>
<button class="tab" id="t-limits" role="tab" data-key="limits" aria-controls="p-limits" aria-selected="false" tabindex="-1">한계와 흐름</button>
</div>
</div>
<!-- ══ 개요 ══════════════════════════════════════════════════════ -->
<section class="panel" id="p-overview" role="tabpanel" aria-labelledby="t-overview" tabindex="-1">
<h2>한 줄로</h2>
<p>
<strong>SUM Parts는 데이터셋이다. 모델이 아니다.</strong>
새 신경망을 만든 논문이 아니라, <em>기존 모델들을 채점할 새 시험지</em>를 만든 논문이다.
CVPR 2025, TU Delft.
</p>
<div class="note">
<strong>가장 흔한 오해</strong>
<p>
"SUM Parts를 쓴다" = 데이터로 모델을 학습시킨다는 뜻이다.
실제로 예측을 내놓는 건 <code>PointVector</code> 같은 별개 모델이고,
그건 다른 팀이 2023년에 발표한 것이다.
</p>
</div>
<h2>먼저 알아야 할 용어</h2>
<dl class="def">
<dt>메시 (mesh)</dt>
<dd>삼각형 면을 이어 붙여 만든 3D 표면. 드론 사진측량 결과물이 보통 이 형태다.
각 면에 사진 텍스처가 입혀진다.</dd>
<dt>포인트 클라우드</dt>
<dd>면 없이 점만 있는 3D 데이터. 신경망 대부분이 이걸 입력으로 받는다.
메시를 쓰려면 표면에서 점을 뽑아 변환한다.</dd>
<dt>시맨틱<br>세그멘테이션</dt>
<dd>모든 점(또는 픽셀)에 "이건 건물, 이건 나무" 하고 이름을 붙이는 일.
물체 하나를 통째로 찾는 <em>객체 검출</em>과 다르다.</dd>
<dt>part-level</dt>
<dd>SUM Parts의 핵심. 건물을 통으로 "건물"이라 하지 않고
<em>외벽·지붕·굴뚝·발코니</em>로 쪼갠다. 이전 SUM(2021)은 객체 단위였다.</dd>
<dt>IoU</dt>
<dd>정답과 예측이 겹치는 정도. <code>겹친 것 ÷ 합친 것</code>.
클래스별로 구하고 평균 낸 것이 <strong>mIoU</strong>다.</dd>
</dl>
<h2>무엇을 제공하나</h2>
<div class="scroll">
<table>
<thead><tr><th>제공</th><th>내용</th></tr></thead>
<tbody>
<tr><td class="win">✓ 데이터셋</td><td>2.5 km² 도시 텍스처 메시, 21개 클래스, 라벨 완비</td></tr>
<tr><td class="win">✓ 벤치마크 점수표</td><td>기존 모델 13종을 태워본 결과 — 어느 모델이 좋은지</td></tr>
<tr><td class="win">✓ 학습 코드</td><td>PointNeXt 번들 (모델 구현 + 학습 스크립트)</td></tr>
<tr><td class="bad">✗ 학습된 가중치</td><td>없다. <strong>직접 학습해야 한다</strong></td></tr>
<tr><td class="bad">✗ 어노테이션 도구</td><td>비공개. 저자가 유료 서비스로 판매 중</td></tr>
<tr><td class="bad">✗ 채점 스크립트</td><td>TODO. test 세트 채점은 저자에게 요청</td></tr>
</tbody>
</table>
</div>
</section>
<!-- ══ 데이터셋 ═══════════════════════════════════════════════════ -->
<section class="panel" id="p-data" role="tabpanel" aria-labelledby="t-data" tabindex="-1" hidden>
<h2>트랙이 두 개다</h2>
<p>
라벨을 <em>어디에</em> 붙였느냐로 갈린다. 데이터도 클래스 수도 다르다.
</p>
<div class="scroll">
<table>
<thead><tr><th>트랙</th><th>라벨 부착 대상</th><th class="num">클래스</th><th>세부 수준</th></tr></thead>
<tbody>
<tr><td><strong>face</strong></td><td>삼각형 면</td><td class="num">13</td><td>외벽 · 지붕 · 굴뚝 · 발코니</td></tr>
<tr><td>texture</td><td>텍스처 픽셀</td><td class="num">19</td><td>위 + 창문 · 문 · 노면표시</td></tr>
</tbody>
</table>
</div>
<div class="note">
<strong>우리는 face 트랙만 쓴다</strong>
<p>
건물·수목·차량·지면만 필요하다. 창문이나 노면표시 단위는 필요 없다.
도로면은 face 트랙에서 <code>terrain</code>에 포함된다.
</p>
</div>
<h3>face 트랙 13클래스</h3>
<div class="flow">0 unclassified 미분류
1 terrain 지면 (도로 포함)
2 high_vegetation 수목
3 facade_surface 건물 외벽
4 water 수면
5 car 차량
6 boat 선박
7 roof_surface 지붕
8 chimney 굴뚝
9 dormer 지붕창
10 balcony 발코니
11 roof_installation 지붕 설비
12 wall 수직 구조물 (옹벽·담장)</div>
<div class="warn">
<strong>3과 12를 헷갈리면 안 된다</strong>
<p>
<code>facade_surface</code><em>건물</em> 외벽이고,
<code>wall</code>은 건물에 속하지 않은 수직 구조물이다 — 옹벽, 방음벽, 담장.
도로 프로젝트에서 옹벽은 지형이 아니라 구조물이므로 이 구분이 중요하다.
</p>
</div>
</section>
<!-- ══ 포인트 클라우드 ════════════════════════════════════════════ -->
<section class="panel" id="p-cloud" role="tabpanel" aria-labelledby="t-cloud" tabindex="-1" hidden>
<h2>점 하나에 뭐가 들어 있나</h2>
<p>
포인트 클라우드의 속성은 두 층으로 나눠 봐야 한다.
<strong>파일에 실제로 저장돼 있는 것</strong>과,
<strong>거기서 계산해 만들어내는 것</strong>이다.
</p>
<h3>1층 · 센서가 주는 원본 속성</h3>
<p>
점 하나 = 숫자 몇 개짜리 행. 뭐가 들어 있는지는 <em>어떤 센서로 찍었나</em>에 달렸다.
</p>
<div class="scroll">
<table>
<thead><tr><th>속성</th><th></th><th>어디서 나오나</th></tr></thead>
<tbody>
<tr><td class="key">x, y, z</td><td>3D 좌표</td><td>항상 있음. 유일한 필수 항목</td></tr>
<tr><td class="key">intensity</td><td>반사 강도</td><td>LiDAR. 금속은 밝고 아스팔트는 어둡다</td></tr>
<tr><td class="key">r, g, b</td><td></td><td>RGB-D 카메라, 사진측량, 카메라 융합</td></tr>
<tr><td class="key">return_number</td><td>몇 번째 되돌아온 신호인가</td><td>LiDAR. 나뭇잎 뚫고 땅까지 여러 번 튐</td></tr>
<tr><td class="key">ring / laser_id</td><td>몇 번 레이저 채널인가</td><td>회전식 LiDAR (Velodyne 64채널 등)</td></tr>
<tr><td class="key">scan_angle</td><td>스캔 각도</td><td>LiDAR</td></tr>
<tr><td class="key">timestamp</td><td>찍힌 시각</td><td>움직이는 물체 분리에 씀</td></tr>
<tr><td class="key">label / class</td><td>정답 라벨</td><td>사람이 붙인 것. 학습용 데이터만</td></tr>
</tbody>
</table>
</div>
<div class="flow"># .las — LiDAR 표준
x=12.34 y=56.78 z=1.20 intensity=8421 return=1/3 ring=17
# .ply — 스캔 / 재구성
x=0.12 y=0.44 z=2.01 r=180 g=140 b=90 nx=0.0 ny=0.0 nz=1.0
# 실내 벤치마크 (S3DIS, ScanNet)
x y z r g b + semantic label + instance id</div>
<div class="note">
<strong>좌표 3개 말고는 다 선택 사항</strong>
<p>
그래서 딥러닝 모델은 보통 <code>xyz</code>만 있어도 돌아가게 설계한다.
SUM Parts는 메시에서 점을 뽑기 때문에 <code>xyz</code> + 텍스처에서 가져온 <code>rgb</code>를 쓴다.
</p>
</div>
<h2>2층 · 계산해서 만드는 기하 특징</h2>
<p>
"이 점은 모서리 같다", "평평한 면 위에 있다" 같은 판단이 여기서 나온다.
센서가 알려주는 게 아니라 <strong>주변 이웃점을 보고 직접 계산</strong>한다.
</p>
<h3>계산 방법</h3>
<p>
이웃점 K개를 모아 <strong>공분산 행렬</strong>을 만들고 고유값 3개를 뽑는다 (λ₁ ≥ λ₂ ≥ λ₃).
고유값 = "주변 점들이 어느 방향으로 얼마나 퍼져 있나".
</p>
<div class="flow">λ₁ 크고, λ₂ λ₃ 작음 한 방향으로만 퍼짐 선 전선, 기둥 모서리
λ₁ λ₂ 크고, λ₃ 작음 두 방향으로 퍼짐 면 벽, 바닥, 지붕
λ₁ ≈ λ₂ ≈ λ₃ 사방으로 퍼짐 덩어리 나뭇잎, 덤불</div>
<div class="scroll">
<table>
<thead><tr><th>이름</th><th>공식</th><th>크면 뭐냐</th></tr></thead>
<tbody>
<tr><td>Linearity</td><td class="key">(λ₁−λ₂)/λ₁</td><td>선형 구조 — 전선, 난간</td></tr>
<tr><td>Planarity</td><td class="key">(λ₂−λ₃)/λ₁</td><td>평면 — 벽, 바닥, 지붕</td></tr>
<tr><td>Sphericity</td><td class="key">λ₃/λ₁</td><td>덩어리 — 수관, 덤불</td></tr>
<tr><td>Curvature</td><td class="key">λ₃/(λ₁+λ₂+λ₃)</td><td>표면 굴곡, 모서리</td></tr>
<tr><td>Anisotropy</td><td class="key">(λ₁−λ₃)/λ₁</td><td>방향성 있음</td></tr>
<tr><td>Omnivariance</td><td class="key">(λ₁λ₂λ₃)^⅓</td><td>전체 부피감</td></tr>
<tr><td>Eigenentropy</td><td class="key">−Σ λᵢ·ln λᵢ</td><td>무질서한 정도</td></tr>
</tbody>
</table>
</div>
<h3>그 외 자주 쓰는 것</h3>
<div class="scroll">
<table>
<thead><tr><th>이름</th><th></th></tr></thead>
<tbody>
<tr><td class="key">normal (nx,ny,nz)</td><td>표면이 향하는 방향. λ₃의 고유벡터</td></tr>
<tr><td class="key">verticality</td><td>1 − |nz|. 벽이냐 바닥이냐 구분</td></tr>
<tr><td class="key">density</td><td>반경 안에 점이 몇 개인가</td></tr>
<tr><td class="key">height above ground</td><td>지면 기준 높이. 야외 데이터의 강력한 단서</td></tr>
<tr><td class="key">local height range</td><td>이웃점들의 z 최대 최소</td></tr>
</tbody>
</table>
</div>
<div class="warn">
<strong>반경을 얼마로 잡느냐가 값을 통째로 바꾼다</strong>
<p>
반경 10 cm로 보면 벽돌 하나의 굴곡이 보이고, 5 m로 보면 건물 전체가 평면으로 보인다.
그래서 여러 반경에서 뽑아 합치는 <strong>multi-scale</strong> 방식을 쓴다.
</p>
</div>
<h2>그래서 신경망이 하는 일</h2>
<p>
위 특징들을 <em>누가 고르느냐</em>가 옛 방식과 딥러닝의 갈림길이다.
</p>
<div class="def">
<dt>손수 고르기<br>(~2015)</dt>
<dd>사람이 planarity·verticality 같은 걸 직접 계산해 Random Forest에 넣는다.
어떤 특징을 쓸지, 반경을 얼마로 할지 전부 사람이 튜닝.</dd>
<dt>학습시키기<br>(PointNet++ 이후)</dt>
<dd><code>xyz</code>(+ 있으면 <code>rgb</code>)만 던져주고
<strong>MLP가 알아서 저런 특징을 학습</strong>하게 한다.</dd>
</div>
<div class="flow">입력 [x, y, z, r, g, b] 6개 숫자 — 사람이 아는 의미
↓ MLP
출력 [0.3, -1.2, 0.8, ...] 64개 숫자 — 이름 없음</div>
<p>
출력 64개에는 이름표가 없다. 3번 채널이 planarity고 17번이 curvature다 —
그런 대응은 없다. 학습 중에 "이 조합이 벽 구분에 유용하더라"를 네트워크가 찾아낸 결과일 뿐이다.
뜯어보면 앞쪽 층이 planarity·linearity 비슷한 걸 학습하는 경향은 있지만 보장된 건 아니다.
</p>
</section>
<!-- ══ 모델 계보 ═════════════════════════════════════════════════ -->
<section class="panel" id="p-models" role="tabpanel" aria-labelledby="t-models" tabindex="-1" hidden>
<h2>채점된 모델들 — 계보</h2>
<p>
포인트 클라우드는 순서도 격자도 없는 점 덩어리다.
이걸 신경망에 어떻게 먹이느냐가 이 계보의 역사다.
</p>
<div class="lineage">
<div class="gen" style="--accent: var(--signal)">
<span class="yr">2017</span><span class="nm">PointNet</span><span class="sc bad">15.1%</span>
<span class="ds">점을 하나씩 따로 보고 마지막에 전부 한 번에 max-pool.
<strong>이웃 개념이 없다.</strong> 원조라는 의미만 있고 실무용은 아니다.</span>
</div>
<div class="gen" style="--accent: var(--terr)">
<span class="yr">2017</span><span class="nm">PointNet++</span><span class="sc">33.1%</span>
<span class="ds">이웃을 반경으로 묶어 지역별로 처리하고 점점 넓게 반복.
CNN이 이미지에서 하는 것과 같은 발상. 두 배 넘게 뛴다.</span>
</div>
<div class="gen" style="--accent: var(--water)">
<span class="yr">2022</span><span class="nm">PointNeXt</span><span class="sc">65.3%</span>
<span class="ds">구조는 거의 그대로 두고 <strong>학습법과 모델 크기만</strong> 개선.
"성능 향상의 대부분은 구조가 아니라 학습법 덕분이었다"는 주장.</span>
</div>
<div class="gen" style="--accent: var(--veg)">
<span class="yr">2023</span><span class="nm">PointVector</span><span class="sc win">70.0%</span>
<span class="ds">PointNeXt 뼈대에 이웃 특징을 스칼라 대신 <strong>고차원 벡터</strong>로 합침.
파라미터는 58%인데 정확도는 더 높다. <strong>우리가 쓰는 모델.</strong></span>
</div>
</div>
<p class="caption">
수치는 논문 Table 3, face 트랙 12클래스 mIoU.
</p>
<h2>MLP는 머신러닝이 아니다</h2>
<div class="def">
<dt>MLP</dt>
<dd><strong>M</strong>ulti-<strong>L</strong>ayer <strong>P</strong>erceptron — 다층 퍼셉트론.
"머신러닝(ML)"의 약자가 아니다. 헷갈리기 쉬운 약자.</dd>
</div>
<p>
신경망 중 가장 단순한 형태다. <code>Linear</code> → 활성화(ReLU) → <code>Linear</code> 를 쌓은 것.
하는 일은 딱 하나 — <strong>숫자 묶음 하나를 받아서 다른 숫자 묶음으로 바꾼다.</strong>
PointNet 계열에서는 보통 1×1 convolution, 즉 점마다 독립으로 도는 작은 MLP다.
</p>
<div class="flow">mlp = nn.Sequential(
nn.Conv1d(C_in, C_mid, 1), nn.BatchNorm1d(C_mid), nn.ReLU(),
nn.Conv1d(C_mid, C_out, 1),
) # 점 하나당 C_in → C_out</div>
<h2>로컬 집계가 방향을 못 본다</h2>
<p>PointNet++이 지역 특징을 만드는 절차는 이렇다.</p>
<div class="flow">1 중심점 하나를 정한다
2 주변 이웃 K개(보통 32개)를 모은다
3 이웃 전원에게 똑같은 MLP를 돌린다 ← 가중치가 하나뿐
4 결과를 max 또는 mean으로 합친다 ← 여기서 방향이 사라진다</div>
<p>
4번이 문제다. 채널별 최댓값만 남기면 <strong>누가 그 값을 냈는지가 사라진다</strong>
위쪽 이웃인지 왼쪽 이웃인지 구분이 안 된다.
이게 <strong>등방적(isotropic)</strong>, 즉 어느 방향이든 똑같이 취급한다는 뜻이다.
</p>
<div class="warn">
<strong>왜 나쁜가</strong>
<p>
벽과 바닥이 만나는 모서리를 생각해 보자.
"위쪽엔 아무것도 없고 옆으로만 평평하다"는 정보가 있어야 모서리인 줄 아는데,
방향을 뭉개면 "주변에 평평한 점들이 있다"까지밖에 모른다.
</p>
</div>
<p>
대조군은 일반 이미지 CNN이다. 3×3 필터는 칸마다 다른 가중치를 쓴다.
</p>
<div class="flow">-1 0 +1
-2 0 +2 "왼쪽은 어둡고 오른쪽은 밝다" = 세로 경계선 검출
-1 0 +1</div>
<p>
방향마다 다르게 반응한다 — 이게 <strong>비등방(anisotropic)</strong>이다.
MLP + max 방식은 이웃마다 다른 가중치를 줄 방법이 구조상 없다.
이웃의 상대 좌표(<code>p_j p_i</code>)를 입력에 같이 넣어 힌트는 주지만,
그건 방향을 <em>재료로</em> 알려주는 것이지 방향별로 <em>다르게 처리</em>하는 게 아니다.
</p>
<h3>기존 해결책이 비싼 이유</h3>
<div class="scroll">
<table>
<thead><tr><th>방식</th><th>방향 구분</th><th>왜 비싼가</th></tr></thead>
<tbody>
<tr><td>MLP + max<br><span class="caption">PointNet++</span></td><td class="bad">✗ 못 함</td><td>대신 싸다</td></tr>
<tr><td>Attention</td><td class="win">✓ 됨</td><td>이웃마다 중요도를 매번 계산 — QKV 행렬 추가, 점 × 이웃 K개 전부 연산</td></tr>
<tr><td>Dynamic Conv</td><td class="win">✓ 됨</td><td>입력마다 필터를 새로 생성 — 필터 생성기 신경망이 따로 필요</td></tr>
</tbody>
</table>
</div>
<p>
점구름은 점이 수만~수십만 개라 이 비용이 그대로 곱해진다.
자율주행이나 로봇 같은 실시간 처리에서는 감당이 안 된다.
<strong>"싸면서도 방향을 구분하는 법"</strong> — PointVector가 벡터 집계로 노리는 지점이 여기다.
</p>
</section>
<!-- ══ PointVector ═══════════════════════════════════════════════ -->
<section class="panel" id="p-vector" role="tabpanel" aria-labelledby="t-vector" tabindex="-1" hidden>
<h2>PointVector의 답 — 특징을 벡터로 만든다</h2>
<p>
앞 탭의 문제는 이것이었다. 방향을 구분하려면 attention이나 dynamic conv가 필요한데
둘 다 비싸다. PointVector는 <strong>기계를 추가하지 않고</strong> 방향을 얻는다.
</p>
<div class="note">
<strong>한 문장</strong>
<p>
특징값(숫자 하나)을 <strong>방향을 가진 3D 벡터</strong>로 바꾼다.
벡터는 원래 방향이 있으니, 더할 때 방향이 저절로 작용한다.
</p>
</div>
<h3>스칼라를 벡터로 — 두 단계</h3>
<div class="flow">1 숫자 f 하나를 축 위의 벡터로 놓는다 (0, f, 0)
2 회전각 두 개(α, β)로 돌린다 → 임의 방향의 3D 벡터</div>
<p>
회전각 <code>α</code>, <code>β</code>는 MLP가 예측한다.
입력은 이웃과의 <strong>상대 위치</strong>(<code>p_j p_i</code>)와
<strong>상대 특징</strong>(<code>f_j f_i</code>)이다.
즉 "이 이웃이 어디에 어떻게 놓여 있느냐"가 벡터 방향을 결정한다.
</p>
<div class="note">
<strong>왜 회전행렬을 직접 예측하지 않나</strong>
<p>
3×3 행렬 원소 9개는 서로 종속이다 (직교 조건 때문에 아무 값이나 못 넣는다).
그런 걸 신경망에 예측시키면 최적화가 어렵다.
<strong>각도 두 개는 서로 독립</strong>이라 자유롭게 움직여도 항상 유효한 회전이 된다.
논문이 <em>"facilitates the network to find the better solution"</em>이라 쓴 지점이다.
</p>
</div>
<h3>VPSA 모듈 — 세 동작</h3>
<div class="flow">Vector encoder 스칼라 c개 → 3D 벡터 c개 (회전 α, β 적용)
Aggregation 이웃 벡터들을 합침 ← 방향이 여기서 작용
Project 벡터 c개 → 스칼라 c개 (group conv)</div>
<p>
마지막에 다시 스칼라로 되돌리는 이유는, 다음 층이 일반 특징을 받아야 하기 때문이다.
벡터는 <strong>집계하는 순간에만</strong> 쓰인다.
</p>
<h3>그래서 싸다</h3>
<div class="scroll">
<table>
<thead><tr><th>방식</th><th>방향 구분</th><th>추가 비용</th></tr></thead>
<tbody>
<tr><td>MLP + max</td><td class="bad"></td><td>없음</td></tr>
<tr><td>Attention</td><td class="win"></td><td>QKV 행렬 3개 + 이웃 전체 내적</td></tr>
<tr><td>Dynamic Conv</td><td class="win"></td><td>필터 생성 신경망</td></tr>
<tr><td><strong>PointVector</strong></td><td class="win"></td><td><strong>각도 2개 예측</strong></td></tr>
</tbody>
</table>
</div>
<p>
이것이 <strong>PointNeXt 파라미터의 58%로 더 높은 정확도</strong>가 나온 이유다
(S3DIS Area 5에서 72.3% mIoU).
무거운 기계를 붙이는 대신, 이미 있는 것을 <em>방향 있는 형태로 바꿨을</em> 뿐이다.
</p>
<h3>우리 과제와 어떻게 닿나</h3>
<div class="note">
<strong>지면과 벽의 구분은 본질적으로 방향 문제다</strong>
<p>
지면 = 이웃이 <strong>수평</strong>으로 퍼져 있음 ·
외벽 = 이웃이 <strong>수직</strong>으로 퍼져 있음.
등방적 집계는 "주변에 점이 많다"까지만 알고 어느 방향인지를 못 본다.
</p>
</div>
<p>
우리 측정치가 이 얘기를 뒷받침한다. 비지면이 지면으로 샌 26,217 포인트 중
<strong>49.90%가 <code>facade_surface</code></strong>였다 —
벽 하단, 지면과 만나는 경계다. 수평 이웃과 수직 이웃이 섞이는 바로 그 구간이다.
</p>
</section>
<!-- ══ Bare Earth ════════════════════════════════════════════════ -->
<section class="panel" id="p-bare" role="tabpanel" aria-labelledby="t-bare" tabindex="-1" hidden>
<h2>Bare Earth — 문제를 다시 세우면</h2>
<p>
최종 목표가 <strong>구조물을 걷어낸 지형</strong>이라면,
13클래스도 4클래스도 필요 없다. <strong>지면 / 비지면 둘</strong>이면 된다.
</p>
<p>
그러면 어려운 곳은 전부 <strong>"지면과 X가 만나는 경계"</strong>로 정리된다.
그런데 그 경계들이 성질이 서로 다르다 — 이게 핵심이다.
</p>
<div class="scroll">
<table>
<thead><tr><th>경계</th><th>성질</th><th>난이도</th></tr></thead>
<tbody>
<tr><td>지면 </td><td>법선이 급변 (수평→수직)</td><td class="win">쉬움 · 기하만으로도</td></tr>
<tr><td>지면 수목</td><td>수직 이격, 단 캐노피가 위를 덮음</td><td>중간</td></tr>
<tr><td>지면 자동차</td><td>지면 위에 얹힌 닫힌 물체</td><td>중간</td></tr>
<tr><td>지면 지장물</td><td>위와 같음</td><td>중간</td></tr>
<tr><td><strong>지면 비탈면</strong></td><td><strong>둘 다 지면.</strong> 잘라내면 안 됨</td><td class="bad">가장 어려움</td></tr>
</tbody>
</table>
</div>
<div class="warn">
<strong>비탈면이 함정이다</strong>
<p>
나머지 넷은 "잘라내기"인데 비탈면만 "잘라내면 안 되는 것"이다. 성질이 반대다.
<strong>"수평이면 지면"</strong>이라는 직관으로 가면 절토·성토 비탈면을 통째로 날린다.
도로 노선은 양쪽이 전부 비탈면이라 치명적이다.
</p>
</div>
<h3>고전 기법이 이미 있다</h3>
<p>
Bare earth 추출은 LiDAR 분야에서 20년 넘은 문제다. 학습이 필요 없는 기하 기법들이 있다.
</p>
<div class="scroll">
<table>
<thead><tr><th>기법</th><th>발상</th></tr></thead>
<tbody>
<tr><td><strong>CSF</strong><br><span class="caption">Cloth Simulation Filter</span></td>
<td>점군을 뒤집고 위에서 <strong>천을 덮어</strong> 늘어뜨린다. 천이 닿는 곳이 지면</td></tr>
<tr><td>PMF</td><td>윈도우를 키워가며 형태학적 열림 연산</td></tr>
<tr><td>TIN 반복 조밀화</td><td>최저점으로 삼각망을 만들고 점진적으로 점을 추가</td></tr>
</tbody>
</table>
</div>
<p>
<strong>CSF가 비탈면에 강하다.</strong> 천이 경사를 따라 늘어지므로 경사면을 지면으로 유지한다.
하지만 고전 기법은 <em>무엇인지</em>를 모른다 — 나무 밑동·차량·옹벽을 형상만으로 판단하니
애매하면 틀린다.
</p>
<h3>그래서 조합</h3>
<div class="flow">신경망 (의미) → 나무 · 차량 · 건물 제거 "이건 나무다"
CSF (기하) → 남은 것에서 비탈면 보존 "이건 경사진 지면이다"</div>
<p>
신경망이 <strong>무엇인지</strong>를 알려주고, CSF가 <strong>어디까지가 지면인지</strong>
결정한다. 서로의 약점을 메운다.
</p>
<h3>검증 가능한 가설 — 이진 학습</h3>
<p>
클래스를 합칠수록 수치가 올랐다. 지금은 <strong>추론 후 합치기</strong>만 한 결과다.
</p>
<div class="scroll">
<table>
<thead><tr><th>클래스 수</th><th class="num">mIoU</th><th>방식</th></tr></thead>
<tbody>
<tr><td>13</td><td class="num">51.83%</td><td>원본 학습</td></tr>
<tr><td>4</td><td class="num">72.14%</td><td>추론 후 통합</td></tr>
<tr><td><strong>2</strong></td><td class="num">?</td><td><strong>학습부터 이진</strong> — 미검증</td></tr>
</tbody>
</table>
</div>
<p>
학습부터 2클래스로 하면 모델이 <code>dormer</code> vs <code>balcony</code> 구분에
용량을 쓰지 않고 <strong>전부 지면/비지면 경계에 쓴다.</strong>
구현은 데이터로더에서 라벨을 remap하는 정도다.
</p>
<div class="warn">
<strong>단, SUM으로는 비탈면을 배울 수 없다</strong>
<p>
SUM Parts는 <strong>헬싱키</strong>다. 평지 도시라 도로 절·성토 비탈면이 거의 없다.
모델이 "경사진 지면"을 본 적이 없으면 인식할 근거가 없다.
<strong>재학습으로도 안 고쳐진다</strong> — 데이터에 없는 개념이다.
비탈면만큼은 기하 기법이 더 신뢰할 만하다.
</p>
</div>
</section>
<!-- ══ 결과 ══════════════════════════════════════════════════════ -->
<section class="panel" id="p-results" role="tabpanel" aria-labelledby="t-results" tabindex="-1" hidden>
<h2>우리가 실제로 확인한 것</h2>
<p>RTX 3060 12GB에서 직접 학습·평가한 결과다.</p>
<div class="scroll">
<table>
<thead><tr><th>모델</th><th class="num">논문</th><th class="num">우리</th><th>비고</th></tr></thead>
<tbody>
<tr><td>PointNet</td><td class="num">15.1%</td><td class="num win">17.19%</td><td>논문 설정 그대로 — <strong>재현 확인</strong></td></tr>
<tr><td>PointVector</td><td class="num">70.0%</td><td class="num">51.83%</td><td>VRAM 부족으로 설정 축소</td></tr>
</tbody>
</table>
</div>
<p>
PointNet이 논문값을 넘긴 것이 중요하다. <strong>파이프라인이 맞다는 증거</strong>다.
PointVector가 못 미친 건 <code>voxel_max</code>를 64000 → 24000으로 낮춰서다.
논문 설정은 16.5 GB가 필요한데 카드가 12 GB다.
</p>
<h3>우리 과제 기준으로 다시 보면</h3>
<p>
13클래스를 우리가 필요한 4개로 합치면 수치가 달라진다.
<code>dormer</code>·<code>balcony</code> 같은 세부 클래스의 실패가
전부 "건물" 안으로 흡수되기 때문이다.
</p>
<div class="scroll">
<table>
<thead><tr><th>클래스</th><th class="num">IoU</th><th class="num">precision</th><th class="num">recall</th></tr></thead>
<tbody>
<tr><td>수목</td><td class="num win">90.70%</td><td class="num">95.11%</td><td class="num">95.14%</td></tr>
<tr><td>건물</td><td class="num win">86.10%</td><td class="num">88.01%</td><td class="num">97.54%</td></tr>
<tr><td>지면</td><td class="num">60.69%</td><td class="num win">95.24%</td><td class="num">62.59%</td></tr>
<tr><td>차량</td><td class="num">51.05%</td><td class="num">78.81%</td><td class="num">59.17%</td></tr>
</tbody>
</table>
</div>
<p>
세부 13클래스로는 51.83%지만, <strong>4클래스로는 72.14%</strong>다.
목표가 bare earth라면 <strong>지면 precision 95.24%</strong>가 핵심 숫자다 —
지면이라 부른 것의 95%가 실제 지면이라는 뜻이다.
</p>
</section>
<!-- ══ 한계와 흐름 ═══════════════════════════════════════════════ -->
<section class="panel" id="p-limits" role="tabpanel" aria-labelledby="t-limits" tabindex="-1" hidden>
<h2>한계 — 반드시 알아야 할 것</h2>
<h3>닫힌 집합 분류기다</h3>
<p>
모델은 13개 점수를 내고 그중 최고를 고른다. <strong>"모르겠음"이 없다.</strong>
학습에서 본 적 없는 것을 만나도 반드시 13개 중 하나를 답한다 —
특징 공간에서 가장 가까운 것으로.
</p>
<div class="warn">
<strong>실제로 겪은 일</strong>
<p>
서산 도로 타일에 추론했더니 <code>water</code> 21%, <code>boat</code> 5%가 나왔다.
물도 배도 없는 현장이다. 헬싱키에서 배운 "물"의 특징 —
어둡고 평평하고 균질하고 수평 — 이 한국 포장면과 맞아떨어진 것이다.
배는 물 위에 있는 것이라 따라왔다. <strong>일관성 있는 착각</strong>이다.
</p>
</div>
<h3>학습 데이터의 세계가 곧 한계다</h3>
<p>
SUM Parts는 헬싱키 도시다. 한국 도로 현장에 그대로 쓰면
<strong>도메인 갭</strong>이 생긴다. 더 좋은 모델로 더 오래 학습해도 이건 안 줄어든다.
해결하려면 한국 데이터에 라벨을 붙여 <strong>파인튜닝</strong>해야 한다.
</p>
<h3>라이선스</h3>
<div class="scroll">
<table>
<thead><tr><th>대상</th><th>라이선스</th><th></th></tr></thead>
<tbody>
<tr><td>데이터셋</td><td>CC BY-NC 4.0</td><td><strong>비상업 한정.</strong> 출처 표기 필수</td></tr>
<tr><td>코드</td><td>GPL-3.0</td><td>파생 배포 시 소스 공개 의무</td></tr>
</tbody>
</table>
</div>
<p class="caption">
NC 데이터로 학습한 가중치도 NC로 취급하는 게 안전하다.
상업 이용은 저자 허락이 필요하다.
</p>
<h2>전체 흐름</h2>
<div class="flow">SUM Parts 데이터 ─┐
├─→ PointVector 학습 ─→ 가중치
PointVector 구조 ─┘ │
서산 OBJ 메시 ─→ 포인트 샘플링 ─→ PLY ─→ 추론 ─→ 클래스별 분리</div>
<p>
왼쪽 두 개는 <strong>한 번만</strong> 하면 되는 자산 구축이고,
아래쪽은 타일마다 반복하는 적용이다.
</p>
</section>
<footer>
논문 <a href="https://arxiv.org/abs/2503.15300">arXiv:2503.15300</a> ·
코드 <a href="https://github.com/tudelft3d/SUM-Parts-Benchmarks">tudelft3d/SUM-Parts-Benchmarks</a> ·
데이터 <a href="https://huggingface.co/datasets/gwxgrxhyz/SUM-Parts">HuggingFace</a><br>
작업 기록은 <code>STATUS.md</code>, 공정 정의는 <code>docs/pipeline.html</code>.
</footer>
</div>
<script>
(function () {
var tabs = [].slice.call(document.querySelectorAll('[role="tab"]'));
var panels = tabs.map(function (t) { return document.getElementById(t.getAttribute('aria-controls')); });
function select(i, moveFocus) {
tabs.forEach(function (t, j) {
var on = j === i;
t.setAttribute('aria-selected', on ? 'true' : 'false');
t.tabIndex = on ? 0 : -1;
panels[j].hidden = !on;
});
if (moveFocus) {
tabs[i].focus();
tabs[i].scrollIntoView({ block: 'nearest', inline: 'nearest' });
}
try { history.replaceState(null, '', '#' + tabs[i].dataset.key); } catch (e) {}
}
tabs.forEach(function (t, i) {
t.addEventListener('click', function () { select(i, false); });
t.addEventListener('keydown', function (e) {
var n = tabs.length, j = -1;
if (e.key === 'ArrowRight') j = (i + 1) % n;
else if (e.key === 'ArrowLeft') j = (i - 1 + n) % n;
else if (e.key === 'Home') j = 0;
else if (e.key === 'End') j = n - 1;
if (j >= 0) { e.preventDefault(); select(j, true); }
});
});
var start = 0, key = (location.hash || '').replace('#', '');
for (var i = 0; i < tabs.length; i++) { if (tabs[i].dataset.key === key) { start = i; break; } }
select(start, false);
})();
</script>