パフォーマンス

前回の記事では、圧縮されたバイト列をすばやくピクセルにデコードする方法を紹介しました。今回はその続きです。デコードしたピクセルで、数億ピクセルの巨大な画像でもドラッグやズームが手の動きに遅れずに滑らかに閲覧できるのはなぜでしょうか。

画像はどこまで大きくなるか

スマートフォンで気軽に撮った写真は 1200 万画素、一眼レフは 2400 万画素ですが、衛星画像は 18641 × 18641 ≈ 3.5 億画素 に達することもあります。もしこれを律儀にすべてメモリへデコードすると:

すべてメモリへデコード ≈ 1.4 GB 3.5 億画素 × 4 バイト 画面に実際に表示できるのは ≈ 800 万画素 4K = 3840 × 2160 ≫

1.4 GB のメモリと数秒の待ち時間——それも、画面に収まるよう縮小され、1 ピクセルずつは肉眼で見分けられない画像を表示するためだけにです。ほとんどのピクセルは見られる機会すらありません。 GuoheView の考え方は、必要な精細さで、必要な範囲にだけ手間をかけるというものです。これは 3 つの仕組みの組み合わせで実現しています:ピラミッド、タイル、オンデマンド読み込みです。

ピラミッド:必要な精細さでデコードする

元の画像を繰り返し半分に縮小し、大きいものから小さいものへ層として積み重ねて保存します。これが画像ピラミッドです。最下層が元のサイズで、上の層ほど辺の長さが半分になり、最後は 256×256 の小さな画像に収まります。18641² の画像ならおよそ 8 層です。

Level 0 — 18641² 元のサイズ Level 1 — 9320² Level 2 … 最上層 ≤ 256² 縮小するほど → 上の層を使う 拡大するほど → 下の層を使う

「ウィンドウに合わせる」で画像を開くと、画像全体が画面に収まるよう縮小されるので、18641² の元データはまったく必要ありません。中間の層のどれかをそのまま表示すれば、速くて軽く済みます。細部を見るために拡大したら、より下の、より精細な層に切り替えます。常にちょうど十分な精細さの層を使う——これがピラミッドの意味です。追加の層はそれぞれ 1 つ下の層の 4 分の 1 の大きさしかないため、すべて合わせても元の画像の 3 分の 1 ほど増えるだけで、コストはわずかです。

タイル:見えている部分だけをデコードする

ピラミッドだけでは十分ではありません。拡大して細部を見るときは最下層の大きな画像を使いますが、それでも数億ピクセルあるからです。そこで各層をさらに 256×256 の小さな正方形(タイル) に分割し、表示時には画面内に入るタイルだけを処理します。

画面の表示領域 表示領域内のタイル:デコード 表示領域外のタイル:後回し 1 枚 256×256。4K の表示領域でも 約 135 枚だけ

4K の表示領域全体を埋めても、作業量はタイル約 135 枚分です。「画像全体で数億ピクセル」とはまったく桁が違います。タイルは並列にデコードすることもでき、この百数十枚を複数の CPU コアに分けて同時に処理すれば、表示がさらに速くなります。

オンデマンド読み込み:ドラッグした先をデコード

ピラミッドとタイルを組み合わせると、「表示領域に駆動される」読み込みの仕組みができあがります。ズームやパンのたびに、キャンバスは現在の表示領域がどの層のどのタイルにかかっているかを計算し、まだデコードしていないタイルをバックグラウンドのスレッドに渡し、デコードが終わったものから 1 枚ずつ貼っていきます。

ズーム / パン 表示領域が変わる 見えるタイルを算出 層を選択 + 交差判定 裏でデコード マルチコア並列 できたら貼る 1 枚ずつ更新 操作を続ける → 繰り返し

デコードはすべてバックグラウンドのスレッドで行うので、画面が固まることはありません。一度デコードしたタイルはキャッシュされるため、ドラッグして戻っても再デコードは不要です。こうして、画像がどれほど大きくても、その瞬間に実際にデコードしているのは常に画面に映る小さな範囲だけになります。

まず見せて、それからくっきりさせる

最後にもう 1 つ、体験の最適化があります。大きな画像に切り替えた瞬間はまだ見えるタイルのデコードが終わっていないため、キャンバスはまず画像全体の低解像度プレビューを下地として敷きます。すぐに全体像が見え、その上からくっきりしたタイルが 1 枚ずつ重なっていくので、精細さは上がる一方です。

ここには見落としやすい落とし穴があります。プレビューとタイルは別々に描画される 2 つの層なので、切り替えのタイミングを誤ると「くっきりしたプレビュー → 一瞬ぼやける → 再びくっきり」という後戻りが起きます。私たちはこの単調にくっきりしていく経路を特に保証しており、途中で後戻りのちらつきが起きることはありません。

まとめ

  • ピラミッドは「どれだけ精細なデータを使うか」を決めます——表示倍率に応じて層を選び、見えない細部に無駄な手間をかけません。
  • タイルは「どの範囲をデコードするか」を決めます——画面に映る小さな範囲だけを扱います。
  • オンデマンド読み込み + バックグラウンド並列化は「いつデコードするか」を決めます——ドラッグした先をデコードし、画面を止めません。
  • 低解像度プレビューの下地は「最初に何を見せるか」を決めます——すぐに画面を出し、それから徐々に精細にします。

この 4 つの組み合わせで、メモリと時間は本当に見えているピクセルにだけ使われます。これが、数億ピクセルの巨大な画像でもドラッグやズームが手の動きに遅れない理由です。