[Astro] #115 Point Cloud Viewer #10 — LAZ圧縮エクスポート・ファイル名反映・進捗表示

[Astro] #115 Point Cloud Viewer #10 — LAZ圧縮エクスポート・ファイル名反映・進捗表示

概要

前回[#114]でOctree全点LODとWorker化を実装したが、エクスポートはLAS(無圧縮)のみだった。今回は自作WASMでLAZ圧縮エクスポートを実現し、エクスポート周りのUX改善も行った。

今回の追加機能

機能概要
LAZエクスポートカスタムlaz-perf WASM(250KB)でLAS→LAZ圧縮出力
ファイル名反映全エクスポートのファイル名をソースファイル名ベースに変更
圧縮進捗表示LAZ圧縮中にスピナー+パーセント表示、Cancelボタン付き

背景 — LAZ圧縮はなぜ難しかったか

Point Cloud ViewerではLAZ読み込み(解凍)はlaz-perfのnpmパッケージで対応済みだった。しかし書き出し(圧縮)のEmscriptenバインディングはnpm版に存在しない。

当初は圧縮用WASMのサイズが大きくViewer同梱は重いと判断して見送っていたが、今回C++ソースに直接バインディングを追加してカスタムWASMをビルドしたところ、250KBに収まった。既存の解凍専用laz-perf.js(88KB)と大差なく、Viewerに同梱する障害がなくなった。

WASMビルドの詳細は別記事にまとめている。

LAZエクスポートの実装

スクリーンショット

Point Cloud Viewer v10 — LAZエクスポートの実装

方針 — 既存LASエクスポートへの最小差分

既存の handleExportLAS はLAS 1.2バイナリをメモリ上で組み立ててBlobダウンロードする処理。ここに asLAZ フラグを追加し、LASバイナリ組み立て後にMemFileWriterで圧縮をかける分岐を入れた。

const handleExportLAS = useCallback(async (asLAZ = false) => {
  // ... 既存のLASバイナリ組み立て(変更なし)...

  if (asLAZ) {
    downloadData = await compressLAStoLAZ(buffer, (percent) => {
      setLoading(`LAZ compressing... ${percent}%`);
    });
  }

  const blob = new Blob([downloadData], { type: 'application/octet-stream' });
  // ...
}, [sourceCRS, exportCRS, files, getExportName]);

compressLAZ.ts

LASバッファを受け取り、ヘッダーからポイントフォーマット・スケール・オフセットを読み取ってMemFileWriterに1点ずつ流し込む。UIフリーズを防ぐため、1万点ごとに setTimeout(0) でUIスレッドに制御を返す。

const CHUNK_SIZE = 10000;

for (let i = 0; i < pointCount; i++) {
  LazPerf.HEAPU8.set(
    lasData.subarray(srcOffset, srcOffset + pointRecordLength),
    pointBuf
  );
  writer.writePoint(pointBuf);

  if (i % CHUNK_SIZE === 0) {
    onProgress?.(Math.floor((i / pointCount) * 100));
    await yieldToUI();
  }
}

圧縮率

398MBのLASファイル(1230万点、Point Format 2)→ 72.6MBのLAZ。**圧縮率18.2%**で、LAZの典型的な範囲(7-20%)に収まっている。

ControlPanelの変更

EXPORTセクションにLAZボタンを追加。グリッド3列で PLY/GLB/LAS/LAZ/PCD/XYZ/CSV の7ボタンになった。

<button style={btnStyle} onClick={onExportLAS}>LAS</button>
<button style={btnStyle} onClick={onExportLAZ}>LAZ</button>

エクスポートファイル名の改善

全エクスポート形式のファイル名を export.xxx 固定から、ソースファイル名ベースに変更した。

const getExportName = useCallback((ext: string) => {
  if (files.length === 0) return `export.${ext}`;
  const base = files[0].name.replace(/\.[^.]+$/, '');
  return files.length === 1 ? `${base}.${ext}` : `${base}_merged.${ext}`;
}, [files]);
  • 単一ファイル: 08NF2340.las08NF2340.laz
  • 複数ファイル: 08NF2340_merged.laz

圧縮進捗表示

既存のローディングUI(スピナー+メッセージ+Cancelボタン)をそのまま流用。setLoading に「LAZ compressing… 20%」のようなメッセージを渡すだけで、解凍時と同じ表示が出る。

if (asLAZ) {
  try {
    setIsProcessing(true);
    downloadData = await compressLAStoLAZ(buffer, (percent) => {
      setLoading(`LAZ compressing... ${percent}%`);
    });
  } finally {
    setIsProcessing(false);
    setLoading(null);
  }
}

変更ファイル

ファイル変更内容
compressLAZ.ts新規 — LAS→LAZ圧縮ラッパー(MemFileWriter + yieldToUI)
laz-perf.js差し替え — カスタムビルド(解凍+圧縮対応)
laz-perf.wasm差し替え — カスタムビルド(250KB)
PointCloudApp.tsxhandleExportLASにasLAZ分岐、getExportNameヘルパー追加、全エクスポートのファイル名変更
ControlPanel.tsxonExportLAZ props追加、LAZボタン追加

まとめ

  • v1: PLY読み込み、ヒートマップ、スライス、計測、SOR
  • v2: LAS/PCD読み込み、200万点サンプリング、正規化
  • v3: LAZ読み込み、XYZ/CSV入出力、ローディング
  • v4: Intensity/Classification表示、LAS/PCD/スクショエクスポート
  • v5: ボックス選択+削除、断面ポリライン+DXF、Octree LOD、座標系変換
  • v6: 計測ラベル3D表示、元座標復元、Voxel Downsampling
  • v7: 任意直線断面+DXF、SOR進捗表示
  • v8: SOR高速化+Worker化、Voxel Worker化、Delaunayメッシュ生成
  • v9: Octree全点LOD(1230万点対応)、Worker化、カメラ追従更新、非LOD上限5M
  • v10: LAZ圧縮エクスポート(自作WASM 250KB)、ファイル名反映、進捗表示

今後の課題

  • Potree形式読み込み(Octreeタイルストリーミング)
  • Poisson Reconstruction(3Dサーフェス復元)
  • GPU Octree(WebGPU compute shader)
  • VLRメタデータ保持(元LASの座標系情報引き継ぎ)