# PCD Viewer 가이드

FAST-LIVO2 대시보드입니다. 두 가지로 씁니다.

1. **실시간** — FAST-LIVO2가 도는 것을 브라우저에서 그대로 봅니다.
   누적되는 맵, 궤적, 현재 포즈, 카메라 영상, 상태 텔레메트리.
2. **사후 계측** — 저장된 `.pcd`를 열어 **거리 / 포지션(ΔXYZ)**을 측정합니다.
   실시간으로 쌓고 있는 맵 위에서도 똑같이 측정됩니다.

**GPS를 입혀 위경도를 표시하는 과정**은 8절에 정리돼 있습니다(아직 미구현).

---

## 1. 파일 위치

| 항목 | 경로 |
|---|---|
| 뷰어 코드 | `~/Documents/pcd viwer/pcd_viewer.html` |
| 포인트 클라우드 | `~/Documents/pcd viwer/garam_good_lite.pcd` |
| 이 문서 | `~/Documents/pcd viwer/PCD_Viewer_가이드.md` |

뷰어는 **단일 HTML 파일**이며, Three.js(0.160)를 CDN(unpkg)에서 불러옵니다.
빌드도 설치도 없습니다. 실시간 기능도 라이브러리 없이 rosbridge 프로토콜을
직접 말합니다(roslibjs 불필요).

> `~/Downloads/pcd_viewer.html`에 예전 버전이 남아 있습니다. **Documents 쪽이 최신**입니다.

---

## 2. 실행 — 뷰어 띄우기

`file://`로 직접 열면 브라우저 보안정책(CORS) 때문에 `.pcd`를 못 읽습니다.
**반드시 로컬 HTTP 서버를 띄워** 접속해야 합니다.

```bash
cd ~/Documents/"pcd viwer"
python3 -m http.server 8000
```

브라우저에서:

```
http://localhost:8000/pcd_viewer.html
```

한 줄로 띄우고 바로 열기(macOS):

```bash
cd ~/Documents/"pcd viwer" && python3 -m http.server 8000 & sleep 1 && open "http://localhost:8000/pcd_viewer.html"
```

서버 종료:

```bash
lsof -ti:8000 | xargs kill
```

> CDN을 쓰므로 **인터넷 연결**이 필요합니다(Three.js 로드용).
> 뷰어는 켤 때 같은 폴더의 `all_raw_points.pcd`를 자동으로 찾습니다.
> 없으면 조용히 "파일 열기" 안내로 넘어가며, **파일 없이 바로 실시간 연결**을 해도 됩니다.

### 클라우드 여는 방법 세 가지
- 같은 폴더에 `all_raw_points.pcd`를 두면 자동 로드
- `파일 열기` 버튼
- `.pcd` 파일을 창에 **드래그 앤 드롭**

---

## 3. 실행 — 실시간 (ROS 2 Humble)

FAST-LIVO2가 도는 그 컴퓨터에서 rosbridge를 띄우면, 브라우저가 토픽을 직접 구독합니다.
**RViz는 필요 없습니다** — 이 대시보드가 RViz 자리를 대신합니다.

### 3-1. rosbridge 설치·실행

```bash
sudo apt install ros-humble-rosbridge-suite
ros2 launch rosbridge_server rosbridge_websocket_launch.xml    # :9090, rosapi 포함
```

`rosapi` 노드가 함께 떠야 합니다(위 launch에 포함). 토픽 목록을 읽는 데 씁니다.

### 3-2. 연결

1. FAST-LIVO2 실행
2. 뷰어 좌측 **LIVE** 칸에 주소 입력 → `연결`
   - 같은 PC에서 볼 때: `ws://localhost:9090`
   - 다른 PC에서 볼 때: `ws://<호스트IP>:9090`
3. 연결되면 **토픽 4칸이 자동으로 채워집니다** (CLOUD / ODOM / PATH / IMAGE)

### 3-3. 토픽은 이름이 아니라 **타입**으로 잡습니다

연결 직후 `/rosapi/topics`로 전체 목록을 받아, 슬롯별로 **메시지 타입이 맞는 토픽만**
드롭다운에 올리고 하나를 자동 선택합니다. 이름은 우선순위 힌트로만 씁니다.

| 슬롯 | 받는 타입 | 이름 우선순위 |
|---|---|---|
| CLOUD | `sensor_msgs/msg/PointCloud2` | `cloud_registered`, `cloud_map`, `laser_map` |
| ODOM | `nav_msgs/msg/Odometry` | `aft_mapped`, `odom` |
| PATH | `nav_msgs/msg/Path` | `path` |
| IMAGE | `sensor_msgs/msg/CompressedImage` | `image`, `rgb`, `color` |

자동 선택이 틀렸으면 드롭다운에서 바꾸면 즉시 재구독합니다. `사용 안 함`으로 끌 수도 있습니다.
**해당 타입의 토픽이 하나도 없으면 그렇다고 말합니다** — 그때는 FAST-LIVO2가 실제로 돌고 있는지,
토픽이 발행 중인지(`ros2 topic list -t`)부터 확인하세요.

> **카메라가 안 보이면**: 지금은 `CompressedImage`만 받습니다.
> FAST-LIVO2가 raw `sensor_msgs/msg/Image`로 낸다면 IMAGE 칸이 비어 있습니다. 우회:
> ```bash
> ros2 run image_transport republish raw in:=/origin_img compressed out:=/origin_img/compressed
> ```

---

## 4. 화면 구성

```
┌──────────────────────────────────────────────────────────┐
│ PCD VIEWER   FAST-LIVO2      LIVE ■ 연결됨 │ PTS │ SOURCE │  ← 레일: 상태
├────────────┬─────────────────────────────────────────────┤
│ LIVE       │                                             │
│ TELEMETRY  │                 3D 뷰포트                    │
│ MODE       │                                             │
│ DISPLAY    │                              ┌────────────┐ │
│ ACTIONS    │                              │  CAMERA    │ │
│ (조작 힌트) │                              └────────────┘ │
└────────────┴─────────────────────────────────────────────┘
```

- **레일(상단)**: LIVE 연결 상태 · PTS(현재 점 개수) · SOURCE(파일명 또는 토픽명)
- **LIVE**: rosbridge 주소, 토픽 바인딩 4칸, 복셀 누적 설정
- **TELEMETRY**: 토픽별 수신 주파수, 복셀 유지율, 누적 버퍼 사용량
- **MODE**: 탐색 / 거리 / 포지션 + 측정 결과 리드아웃
- **DISPLAY**: 점 크기, 원본 색상
- **ACTIONS**: 파일 열기 / 뷰 맞춤 / 초기화
- **CAMERA(우하단)**: 카메라 토픽이 붙어 있을 때만 나타납니다

### 색에 대한 약속
UI는 전부 무채색입니다. **색이 보이면 그건 마커라는 뜻**입니다.

| 색 | 의미 |
|---|---|
| 🔴 적 | 거리 측정의 점 1 |
| 🟢 녹 | 거리 측정의 점 2 / 포지션 모드의 측정점 |
| 🟠 앰버 | 포지션 모드의 기준점 |
| ⚪ 흰색 | 측정선·거리 라벨, 현재 센서 포즈 |
| 회색 | 궤적(PATH), 기준점↔측정점 연결선 |

---

## 5. 사용법

### 마우스 조작
- **좌클릭 드래그**: 회전
- **우클릭 드래그**: 이동(팬)
- **휠**: 확대/축소
- `F` 또는 `뷰 맞춤`: 클라우드 전체가 보이게 카메라 리셋
- `Esc` 또는 `초기화`: 측정 전부 삭제

> 실시간으로 맵이 자라는 동안에는 **자동으로 뷰를 맞춰줍니다.**
> 단, 카메라를 한 번이라도 건드리면 그때부터는 자동 조정을 하지 않습니다(원하는 시점 유지).
> 다시 전체를 보려면 `F`.

### 거리 측정
1. **MODE → 거리** 선택
2. 점 두 개를 차례로 클릭
3. 두 점 사이에 흰 선과 거리(m)가 표시되고, 리드아웃에 소수 4자리까지 나옵니다
4. 세 번째 클릭 또는 `초기화`로 리셋

### 포지션 (기준점 대비 ΔXYZ)
1. **MODE → 포지션** 선택
2. **첫 클릭** = 기준점(앰버) 설정
3. **이후 클릭** = 기준점 대비 `ΔX / ΔY / ΔZ` + 직선거리 표시
4. `초기화`로 리셋

> 모드는 셋 중 하나입니다. **탐색**에서는 클릭해도 점이 찍히지 않습니다(회전만).
> 측정은 **실시간 맵 위에서도 똑같이** 동작합니다.

### 표시
- **점 크기** 슬라이더
- **원본 색상** — 끄면 클라우드가 중성 회색으로 바뀝니다. 색 없는 클라우드면 비활성화됩니다.

### 복셀 누적 (LIVE 칸, 기본 켜짐 / 5cm)
실시간으로 들어오는 스캔은 서로 크게 겹칩니다. **한 복셀에 점 하나만** 남겨서
메모리가 세션 시간이 아니라 **공간 크기**에 비례하게 만듭니다. 자세한 원리는 6-2절.

- **복셀 크기**를 바꾸면 **이미 쌓인 점도 새 격자로 다시 색인**합니다.
  키우면 그 자리에서 솎이고, 줄이면 버려진 점은 못 되살리지만 이후 스캔부터 촘촘해집니다.
- **끄면** 원본 그대로 쌓습니다. 짧고 고밀도인 계측에만 쓰세요 — 버퍼가 빨리 찹니다.
- TELEMETRY의 **복셀** 행에 `5cm · 유지 18.0%`처럼 유지율이 나옵니다.
  유지율이 높게 유지되면(예: 60% 이상) 복셀이 너무 작다는 뜻이니 키우세요.

> 복셀은 **누적 방식**이지 표시 옵션이 아닙니다. 파일로 연 클라우드에는 적용되지 않습니다.

---

## 6. 실시간이 동작하는 방식

### 6-1. 전송
rosbridge v2 프로토콜(`subscribe` / `unsubscribe` / `call_service` / `publish`)을 WebSocket으로
직접 말합니다. 메시지는 **JSON**이고, `PointCloud2.data`와 `CompressedImage.data` 같은
`uint8[]` 필드는 **base64**로 옵니다.

- base64는 바이너리의 약 **1.33배**입니다. localhost·LAN이면 문제없습니다.
  원격에서 무거우면 CBOR 구독으로 올리는 게 다음 수순입니다.
- 이미지만 10Hz로 throttle합니다. **스캔은 throttle하지 않습니다** — 버리면 맵이 손실되니까.

### 6-2. 누적 버퍼
600만 점짜리 GPU 버퍼를 **미리 한 번** 잡아두고, 들어오는 스캔을 뒤에 이어 붙입니다.

- 스캔마다 객체를 만들지 않으므로 드로우 콜은 **끝까지 하나**입니다.
- 매 프레임 전체를 올리지 않고, **새로 붙은 구간만** GPU로 올립니다.
- 600만 점(약 90MB)이 차면 **"가득 참"이라고 말하고 누적을 멈춥니다.** 조용히 버리지 않습니다.
- 더 필요하면 `pcd_viewer.html`의 `LIVE_CAP` 값을 올리세요(메모리는 점당 약 15바이트).

### 6-3. 복셀 누적 — 왜 필요한가
`/cloud_registered`는 **매 스캔을 통째로** 내보냅니다. 같은 벽을 100번 보면 같은 벽이 100번 옵니다.
그대로 쌓으면 버퍼가 **1분 남짓**에 찹니다(스캔당 1만 점 × 10Hz 기준).

그래서 점이 들어올 때마다 좌표를 복셀 격자로 양자화하고, **이미 점이 있는 복셀이면 버립니다.**
결과적으로 누적량이 **"얼마나 오래 돌았나"가 아니라 "얼마나 넓은 공간을 봤나"**에 비례합니다.

- 복셀 키는 축당 17비트로 패킹한 정수 하나입니다(±65,536 복셀 = 5cm에서 ±3.2km).
  이 범위를 벗어난 점은 버리지 않고 중복 검사 없이 그냥 넣습니다.
- 유지율은 TELEMETRY의 **복셀** 행에서 실시간으로 보입니다.

### 6-4. PointCloud2 해석
`fields`의 offset을 그대로 따라 읽습니다. `x/y/z`(float32)는 필수,
색은 `rgb`(packed) → 없으면 `intensity`(회색 계조) → 없으면 균일 회색 순으로 씁니다.
`is_dense: false`인 클라우드의 NaN 점은 건너뜁니다.

### 6-5. 텔레메트리는 실측만
`CLOUD / ODOM / IMAGE`의 Hz는 **메시지 도착 간격**으로 계산합니다(EMA).
**2초간 조용하면 그럴듯한 값 대신 `—`로 떨어집니다.** 값이 보이면 실제로 오고 있다는 뜻입니다.

---

## 7. 거리값이 계산되는 원리

핵심: **PCD의 각 점은 이미 미터 단위의 실측 3D 좌표를 가진다.**

### 7-1. PCD가 담고 있는 것
`garam_good_lite.pcd` 헤더:
```
FIELDS x y z rgb
TYPE F F F U
POINTS 2518822
DATA binary
```
모든 점이 `(x, y, z)` float 좌표 + 색상(rgb)을 가집니다.
이 좌표는 센서 기준 원점에서의 **실제 물리 위치(미터)**입니다.
사진(픽셀)과 달리 스케일이 이미 현실 단위라서 거리 측정이 가능합니다.
실시간 `PointCloud2`도 동일합니다 — 그래서 라이브 맵에서도 그대로 측정됩니다.

### 7-2. 클릭 → 점 찾기 (피킹)
1. 클릭 위치를 정규화 좌표(-1~1)로 변환
2. 카메라에서 그 픽셀을 지나는 **광선(ray)** 생성
3. 수백만 점 중 광선에 **수직으로 가장 가까운 점**(`distanceToRay` 최소)을 선택
   → 화면상 "커서 아래 점"
4. 그 점의 **저장된 원본 좌표** `(x, y, z)`를 읽음

> 피킹 반경은 카메라 거리에 비례합니다. 멀리서 찍으면 엉뚱한 점을 잡기 쉬우니
> **확대해서 클릭**하세요. 5픽셀 이상 움직이면 클릭이 아니라 드래그로 처리됩니다.

### 7-3. 두 점 사이 거리 = 3D 유클리드 거리
점 A `(x₁,y₁,z₁)`, 점 B `(x₂,y₂,z₂)`:

```
d = √[ (x₂-x₁)² + (y₂-y₁)² + (z₂-z₁)² ]
```

- **거리 모드**: 위 식의 `d`
- **포지션 모드의 ΔX/ΔY/ΔZ**: 제곱근 씌우기 전의 축별 성분 `(x₂-x₁)` 등

정확도는 (1) 센서 좌표 자체의 정밀도, (2) 피킹이 의도한 점을 잡았는지에 달립니다.
좌표 단위가 미터라는 가정만 맞으면 결과도 미터입니다.

---

## 8. GPS를 입혀 위경도를 표시하는 과정 (미구현)

### 8-1. 왜 바로는 안 되는가
FAST-LIVO2 출력 포인트는 **로컬 좌표계(미터)**입니다.
- 원점 = SLAM을 시작한 위치
- 축 = 시작 시점의 자세 기준 (IMU 중력 정렬 → **Z축은 중력 반대 방향**, 스케일 ≈ 1m)
- 즉 `(x, y, z)`는 "출발점 대비 상대 위치"일 뿐, 지구상 위치 정보는 없음

위경도로 바꾸려면 **로컬 좌표계를 지구 좌표계에 정렬**(지오레퍼런싱)해야 합니다.

### 8-2. 구해야 하는 변환
실질 미지수는 두 가지:
1. **기준 원점의 위경도** `(lat₀, lon₀, alt₀)`
2. **헤딩(yaw 회전)** — 로컬 X/Y축이 동/북 대비 돌아간 각도

(Z는 이미 중력 정렬되어 있어 roll/pitch는 거의 0)

변환 흐름:
```
로컬 (x,y,z)
   │  회전 + 평행이동
   ▼
ENU (동, 북, 상 / 미터)
   │  측지 변환 (ENU → ECEF → geodetic)
   ▼
(위도, 경도, 고도)
```
ENU↔위경도 변환은 표준 공식이며, 작은 영역이면 근사식으로도 충분합니다.

### 8-3. 변환을 구하는 방법 (정확도 순)

| 방법 | 필요 데이터 | 정확도 |
|---|---|---|
| **A. 시간동기 GPS↔궤적 정합** | FAST-LIVO2 타임스탬프 궤적(pose) + GPS 로그(lat/lon/alt) | GPS 품질 의존. RTK면 cm급, 일반 GPS면 수 m |
| **B. 기준점 수동 지정** | 맵 내 한 점의 실제 위경도 + 진행 방향(헤딩) | 헤딩 정확도에 민감, 수 m~수십 m |
| **C. SLAM 단계 GPS 융합** | GPS 팩터를 그래프에 넣어 재최적화(LIO-SAM/GLIM 등) | 가장 일관적·정확 |

- **방법 A**가 일반적: 궤적의 각 시점 위치와 같은 시각 GPS의 ENU 위치를 짝지어,
  둘을 가장 잘 겹치는 **강체변환(또는 7-DOF 유사변환)**을
  최소제곱(Umeyama/Kabsch)으로 계산. 충분한 이동과 **회전(코너)**이 있어야
  헤딩이 잘 잡힘.
- FAST-LIVO2는 GPS를 직접 융합하지 않으므로, GPS는 **사후 정합** 또는
  **외부 그래프 최적화**로 붙여야 함.

### 8-4. 뷰어 쪽 구현 방향
변환만 정해지면 뷰어 추가는 간단합니다.
- 클릭 시 이미 로컬 `(x,y,z)`를 읽고 있으므로, 거기에 변환을 적용해
  **위/경도/고도를 리드아웃에 함께 표시**.
- 점마다 위경도를 굽지 않고, `{ 원점 lat/lon/alt, 헤딩, 스케일 }` 설정 한 벌만
  저장해두고 클릭 시 실시간 변환 → 가볍고 정확.
- 실시간에도 그대로 적용됩니다. 대시보드에 `GEO` 슬롯(예: `sensor_msgs/msg/NavSatFix`)을
  하나 더 두면 궤적↔GPS 정합을 세션 중에 계산할 수도 있습니다.

### 8-5. 진행을 위해 필요한 것
- **궤적 파일 + GPS 로그**가 있으면 → 방법 A로 변환 계산 + 뷰어 위경도 표시
  (포맷: TUM/KITTI pose, NMEA, CSV 등)
- **기준점 위경도 + 헤딩만 알면** → 방법 B로 설정값 입력 UI 추가
- 우선 **위경도 표시 틀만** 먼저 넣고 설정값은 나중에 채워도 됨

---

## 9. 문제가 생기면

| 증상 | 확인할 것 |
|---|---|
| 화면이 검고 "뷰어를 불러오지 못했습니다" | 인터넷 연결(Three.js CDN). HTTP 서버로 열었는지 |
| `.pcd`가 안 열림 | `file://`로 연 건 아닌지. 8000 포트 서버로 접속 |
| `연결 실패` | 해당 주소에 rosbridge가 떠 있는지. 원격이면 방화벽 9090 |
| `rosapi 응답이 없습니다` | rosbridge에 rosapi 노드가 함께 떠 있는지(launch 파일 확인) |
| 토픽 목록이 비어 있음 | FAST-LIVO2가 실제로 발행 중인지 — `ros2 topic list -t` |
| CAMERA가 안 뜸 | 토픽이 raw `Image`일 수 있음 → 3-3의 republish 참고 |
| Hz가 `—` | 그 토픽이 2초 넘게 조용하다는 뜻(정상 표시). 발행 측 확인 |
| `버퍼 가득 참` | **복셀 크기를 키우세요**(즉시 솎임). 복셀이 꺼져 있으면 켜기. 그래도 부족하면 `LIVE_CAP` 상향 |
| 맵이 너무 성김 | 복셀 크기를 줄이세요. 이미 버린 점은 안 돌아오지만 이후 스캔부터 촘촘해집니다 |

---

## 10. 요약

> **실시간**: rosbridge로 토픽을 직접 구독 → 스캔을 **복셀 단위로 솎아** 한 버퍼에 누적 →
> RViz 없이 브라우저에서 봄. 누적량은 돌린 시간이 아니라 본 공간 크기에 비례.
> 텔레메트리는 실측만 표시하고, 조용하면 조용하다고 말함.
>
> **계측**: PCD = 실측 3D 좌표(미터)의 집합 → 클릭으로 두 점 좌표를 집어 → 피타고라스(3D)로 거리 계산.
> 파일이든 라이브 맵이든 동일.
>
> **위경도**: 별도 GPS 데이터로 로컬↔지구 좌표 변환(지오레퍼런싱)을 한 번 구해두면 표시 가능. 아직 미구현.
