[Astro] #108 Point Cloud Viewer — LAZ圧縮対応&XYZ/CSVインポート/エクスポート
概要
前回、[#107]でLASとPCDに対応した。今回はLAZ(LASzip圧縮)、XYZ/CSVの入出力、ローディングインジケーターを追加して、対応フォーマットを6種類に拡張する。
[Astro] #107 Point Cloud Viewer — LAS/PCD対応&大規模点群サンプリング // PROTOCOL.LAIN
Astro + React Three Fiber製の点群ビューアにLAS/PCDフォーマット対応を追加。LASバイナリパーサーの自前実装(LAS 1.0–1.4, Format 0–3, RGB 8/16bit自動判定)、PCDのASCII/binary/binary_compressed(LZF)対応、Fisher-Yatesランダムサンプリングによる大規模点群の描画、測地座標のセンタリング+正規化の全工程を解説。
lain-lab.comLAZはLASの圧縮版で、測量データの配布ではLAZが主流。圧縮率は7〜15分の1で、386MBのLASが30〜50MBのLAZになる。ただし独自の算術符号化を使っているため、自前パーサーでは対応できず、laz-perfのWASMライブラリを使う。
スクリーンショット
動画(GIF) LAZ
コンポーネント構成
src/components/PointCloudViewer/
├── PointCloudApp.tsx # メインアプリ(state管理)
├── PointCloudScene.tsx # 3D描画・ファイル読み込み振り分け
├── ControlPanel.tsx # GUI操作パネル
├── parseLAS.ts # LASバイナリパーサー
├── parsePCD.ts # PCDパーサー + LZF解凍
├── parseXYZCSV.ts # XYZ/CSVテキストパーサー [NEW]
├── decompressLAZ.ts # LAZ→LAS解凍ラッパー [NEW]
├── laz-perf.js # laz-perf WASMラッパー (ESMパッチ済み) [NEW]
└── laz-perf.d.ts # 型定義 [NEW]
public/
└── PointCloudViewer/
└── laz-perf.wasm # LAZ解凍WASMバイナリ (210KB) [NEW]
LAZ対応の実装
WASMライブラリの選定
LAZの圧縮アルゴリズムは算術符号化ベースで、自前のJavaScript実装は現実的ではない。laz-perfはC++のLAZ実装をEmscriptenでWASMにコンパイルしたもので、ブラウザで使える唯一の選択肢。
npmパッケージとして公開されているが、Astro(Vite)のプロジェクトにnpm installすると @vite-pwa/astro のpeer dependency conflictが発生したため、npmを使わず必要なファイルだけを直接配置した。
Emscripten UMDモジュールのESMパッチ
laz-perf.jsはEmscriptenが生成するUMD(Universal Module Definition)形式で、末尾が以下のようになっている。
if (typeof exports === 'object' && typeof module === 'object')
module.exports = createLazPerf;
else if (typeof define === 'function' && define['amd'])
define([], function() { return createLazPerf; });
else if (typeof exports === 'object')
exports["createLazPerf"] = createLazPerf;
ViteはESMを期待するため、この部分を以下に置き換える。
export default createLazPerf;
これだけで import createLazPerf from './laz-perf.js' が動くようになる。
JSとWASMの配置
Emscriptenモジュールは初期化時にWASMファイルを fetch で取得する。JSファイルはViteがバンドルするが、WASMファイルはバンドルされずに別ファイルとしてブラウザに配信する必要がある。
src/components/PointCloudViewer/laz-perf.js ← Viteがバンドル
public/PointCloudViewer/laz-perf.wasm ← そのまま静的配信
locateFile オプションでWASMのパスを指定する。
const module = await createLazPerf({
locateFile: () => '/PointCloudViewer/laz-perf.wasm',
});
解凍の仕組み
laz-perfのAPIは1点ずつ解凍するインターフェースになっている。
const laszip = new module.LASZip();
laszip.open(inputPtr, lazData.length);
const pointCount = laszip.getCount(); // 点の数
const pointLength = laszip.getPointLength(); // 1点のバイト数
for (let i = 0; i < pointCount; i++) {
laszip.getPoint(pointPtr); // 1点をWASMメモリに解凍
// pointPtrから読み取ってoutputに書き込む
}
解凍結果は元のLASヘッダー+解凍済みの点データで構成されるLAS互換のArrayBufferとして構築し、既存の parseLAS() にそのまま渡す。パーサーの二重実装を避けられる。
チャンク分割による進捗表示
2180万点を1点ずつ解凍するループはブラウザのメインスレッドをブロックする。50万点ごとに setTimeout でUIスレッドに制御を返し、進捗を更新する。
const CHUNK_SIZE = 500_000;
for (let i = 0; i < pointCount; i++) {
laszip.getPoint(pointPtr);
outputBytes.set(pointData, headerSize + i * pointLength);
if (i > 0 && i % CHUNK_SIZE === 0) {
const percent = Math.round((i / pointCount) * 100);
onProgress?.(percent);
await new Promise((r) => setTimeout(r, 0));
}
}
UIには「Decompressing LAZ: 23%」のように表示される。2180万点で約43回の進捗更新が入る。
LAZファイルの判別
LAZファイルはLASと同じ LASF シグネチャを持つため、マジックバイトでは区別できない。拡張子 .laz で判別し、解凍後にマジックバイト判定でLASパーサーに振り分ける。
const isLAZ = file.name.toLowerCase().endsWith('.laz');
if (isLAZ) {
buffer = await decompressLAZ(buffer, (percent) => {
onLoadingStart(`Decompressing LAZ: ${percent}%`);
});
}
// この後、通常のLASF判定 → parseLAS() に流れる
ローディングインジケーター
ファイル読み込み・解凍・パース・サンプリングの各段階でステータスメッセージを表示する。
Loading 1 file(s)...
→ Decompressing LAZ: filename (105.1 MB)...
→ Decompressing LAZ: 23%
→ Parsing: filename...
→ Sampling: 21,812,790 → 2,000,000 points...
→ (完了 → オーバーレイ非表示)
CSSアニメーションのスピナーとステータステキストを半透明オーバーレイで表示する。z-index: 1000 で全面を覆い、処理完了時に setLoading(null) で非表示にする。
XYZ/CSVインポート/エクスポート
フォーマット
XYZはスペース区切り、CSVはカンマ区切りのテキスト形式。
# XYZ(ヘッダーなし、スペース区切り)
0.123456 0.234567 0.345678
0.123456 0.234567 0.345678 128 64 32 ← RGB付き
# CSV(ヘッダーあり、カンマ区切り)
x,y,z,r,g,b
0.123456,0.234567,0.345678,128,64,32
インポート
拡張子(.xyz / .csv / .txt)で判別し、テキストパーサーに振り分ける。CSVの場合は1行目がヘッダーかどうかを自動判定する(非数値を含む場合はヘッダーとみなしてスキップ)。
RGBの値域も自動判定する。最大値が1を超えたら0–255として扱い、1以下なら0–1として扱う。
エクスポート
4種類のエクスポート(PLY / GLB / XYZ / CSV)に対応。XYZとCSVはエクスポートした結果をそのままインポートし直せる(ラウンドトリップ対応)。
テスト結果
| データ | フォーマット | 点数 | 結果 |
|---|---|---|---|
| USGS Ouachita LiDAR | LAZ | 21,812,790 → 2,000,000 | ✅ 約2分で解凍+表示 |
| Export→Import (XYZ) | XYZ | 2,000,000 | ✅ ラウンドトリップ |
| Export→Import (CSV) | CSV | 2,000,000 | ✅ ラウンドトリップ |
対応フォーマット一覧
入力(6種類)
| 形式 | 実装 | 特徴 |
|---|---|---|
| PLY | Three.js PLYLoader | ASCII / Binary |
| LAS | 自前パーサー | 1.0–1.4 / Format 0–3 / RGB |
| LAZ | laz-perf WASM → parseLAS | 圧縮LAS / 進捗表示 |
| PCD | 自前パーサー | ASCII / Binary / LZF圧縮 |
| XYZ | 自前パーサー | スペース区切りテキスト |
| CSV | 自前パーサー | カンマ区切り / ヘッダー自動判定 |
出力(4種類)
| 形式 | 説明 |
|---|---|
| PLY (ASCII) | 頂点座標+頂点カラー |
| GLB | Three.js GLTFExporter |
| XYZ | スペース区切りテキスト |
| CSV | カンマ区切り / ヘッダー付き |
まとめ
LAZ対応でLiDAR測量データの標準的な配布形式を読めるようになった。
実装のポイント:
- Emscripten UMDモジュールは末尾を
export defaultに書き換えるだけでViteで使える - WASMファイルはpublicフォルダに配置して
locateFileでパスを指定 - 大量点の解凍ループはチャンク分割+
setTimeoutでUI応答性を確保 - LAZ/LASはマジックバイトが同じ(LASF)なので拡張子で判別
- XYZ/CSVはテキストベースなのでラウンドトリップ確認が容易