# 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 데이터로 로컬↔지구 좌표 변환(지오레퍼런싱)을 한 번 구해두면 표시 가능. 아직 미구현.