はじめに
暦本研究室のこれまでの研究を俯瞰できるインタラクティブな年表を作りました。大きさは 10198 × 3164 の大判画像1枚。これをWebで快適に閲覧・検索できる状態にするまでの実装と、ボトルネックを潰して本番品質にした記録をまとめます。
作ったサイトはこちら ↓
https://rkmt-chronicle-viewer.vercel.app/
まず触ってみてください。ズーム・パンと / キーでの検索が体験できます。以下はその裏側の話です。
構成は Next.js + OpenSeadragon + Tesseract OCR で、ホスティングは Vercel です。
この記事でわかること
- 大きな1枚画像を「地図品質」で表示する構成
- OCR検索からヒット地点へジャンプする実装
- 体感速度を上げるための段階的最適化
- Vercel運用で詰まりやすい点と回避策
なぜ普通の画像表示では厳しいのか
10K級の画像を <img> でそのまま配信すると、初期ロード・メモリ消費・ズーム時の解像感のどれかが破綻します。Google Maps が地図全体を一度に送らないのと同じで、「今見えている領域だけタイルとして読み込む」方式に寄せるのが定石です。
この用途の定番が OpenSeadragon と DZI(Deep Zoom Image) の組み合わせです。DZI は画像を解像度ピラミッドに分割し、タイル単位で保持するフォーマットで、OpenSeadragon がそれを直接扱えます。
採用アーキテクチャ
全体の構成は次のとおりです。
- 元画像を
DZI + tilesに変換し、静的ファイルとして配信する - OCR結果を
bbox付きJSONとして静的配信する - 検索でヒットした
bboxにビューをアニメーション移動する - 該当領域をオーバーレイで強調する
- URLクエリ
?q=でリンク共有・再現を可能にする
サーバーサイド処理は不要で、すべて静的配信+クライアント処理で完結します。
実装ステップ
1. Deep Zoom タイル生成
画像ピラミッドは libvips の dzsave で一発生成できます。
vips dzsave images/source.png public/tiles/timeline \
--tile-size=256 --overlap=1 --suffix=.jpg[Q=85]
実行すると timeline.dzi(メタデータXML)と timeline_files/ 以下にレベル別タイル群が出力されます。タイルサイズ 256px、オーバーラップ 1px、JPEG品質 85 は多くのケースで妥当な出発点です。
2. OCR インデックス生成
Tesseract を TSV モードで実行し、各テキスト領域の座標(left, top, width, height)と認識テキストを取得します。
tesseract images/source.png out/scan -l eng --psm 11 --dpi 300 tsv
TSV をパースして、以下のような JSON に正規化します。
{
"id": "line-1:1:1:1",
"text": "Incremental Gaussian Splatting",
"norm": "incremental gaussian splatting",
"context": "Incremental Gaussian Splatting ...",
"bbox": [8120, 1680, 520, 42],
"conf": 0.93,
"kind": "line"
}
ここで重要なのは、bbox を 元画像のピクセル座標 で保持することです。OpenSeadragon は内部で正規化座標(画像幅を 1 とする比率)を使うため、表示時に変換する必要がありますが、元座標を持っておけば他の用途にも転用しやすくなります。
norm は小文字化した検索用フィールド、context は周辺テキストを結合した文脈表示用フィールドです。この前処理をビルド時にやっておくことで、クライアント側の負荷を減らせます。
3. Viewer と検索の結合
表示と検索の接続は次の3要素で構成しています。
Viewer(表示): OpenSeadragon で DZI を読み込みます。tileSources に .dzi ファイルのパスを渡すだけで、ズーム・パン・タイル読み込みは自動で処理されます。
Search(検索): Fuse.js でファジー検索を行います。完全一致だけでなく、タイポや部分一致にも対応できるので、OCR の認識誤差を吸収する効果もあります。
Jump + Highlight(移動と強調): 検索ヒット時に viewport.fitBounds() で該当領域にアニメーション移動し、addOverlay() で矩形を重ねて視認性を確保します。
// bbox(元画像座標)からOpenSeadragonのRectに変換して移動
const rect = viewer.viewport.imageToViewportRectangle(
bbox[0], bbox[1], bbox[2], bbox[3]
);
viewer.viewport.fitBounds(rect.rotate(0), true);
4. UX 実装
ビューアの操作性に直結した改善をいくつか入れました。
-
/キーで検索フォーカス。ページ内検索に近い手触りを意識しました -
↑/↓で候補移動、Enterで確定 -
n/pで次・前のヒットを巡回(複数ヒット時の探索用) -
Escと検索エリア外クリックで検索を閉じる - 初回訪問時のみショートカットヒントを表示
- 候補リストに
context(周辺テキスト)を表示して、どの領域のヒットか判断しやすくする
キーボードショートカットは導入コストが低い割に、繰り返し使うユーザーの体験を大きく改善します。
5. 体感速度の改善
最初の実装では OCR JSON の読み込みから検索処理まですべてメインスレッドで行っていましたが、画像が大きいと JSON サイズも無視できず、UI のもたつきが気になりました。以下の順に改善しています。
① OCR 生成時に context を前処理する
検索候補に文脈を表示するために周辺テキストを結合する処理を、最初はクライアント側で毎回やっていました。これをビルドスクリプト側に移し、JSON に context フィールドとして焼き込むことで、クライアントの初期化処理を削減しました。
② 検索処理を Web Worker に移動する
Fuse.js のインデックス構築と検索実行を Web Worker に逃がしました。メインスレッドは UI 描画に専念でき、検索中の入力遅延が解消されます。
③ OCR JSON を lines.json と words.json に分割する
行単位のデータ(lines.json)はファイルサイズが小さく、大半の検索はこれで十分です。単語単位の words.json は、より細かい検索が必要になったときだけ読み込みます。
④ 初期ロードは lines のみ、検索開始時に words を遅延ロードする
ページ表示時には lines.json だけ取得し、ユーザーが検索を開始した時点で words.json をバックグラウンドで取得します。初期表示に不要なデータの取得を遅延させることで、体感速度が改善しました。
この4段階の改善で、メインスレッド負荷の低減と初期ロードコストの削減を両立できました。
6. Vercel 運用での注意点
Vercel にデプロイする際に詰まりやすいポイントをまとめます。
画像最適化の制約を把握する: Vercel の Image Optimization には処理対象画像のサイズ・寸法に制約があります。10K級の元画像をそのまま最適化に通すのではなく、事前にタイル化して静的ファイルとして配信する今回の方式のほうが安全です。
タイルには長期キャッシュを設定する: タイル画像は内容が変わらない静的アセットなので、積極的にキャッシュさせます。
// next.config.js の headers 設定例
{
source: '/tiles/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable',
},
],
}
OCR JSON は中程度の TTL にする: OCR 結果はデータ更新の可能性があるため、タイルほど長いキャッシュは付けず、更新しやすい TTL にしておきます。
7. この構成で得られたもの
最終的に、次の体験が実現できました。
- 10K級の画像でも初期表示が軽い(必要なタイルだけ取得するため)
- どこまで拡大しても解像感が維持される
- テキスト検索 → 該当箇所へジャンプ → 文脈理解、という一連の体験が成立する
- すべて静的配信中心で、Vercel の運用がシンプルに保てる
まとめ
Deep Zoom 単体の導入記事は既にありますが、実際の価値は「検索導線」と「運用を見据えた最適化」を組み合わせたときに出てきます。
今回の構成はインタラクティブ年表に使いましたが、デジタルアーカイブ、大判ポスター、地図的UIの可視化など、「大きな画像の中から情報を探す」体験が必要な場面にそのまま転用できるはずです。
ソースコード
実装の全体は GitHub で公開しています。
https://github.com/keigo1110/rkmt_chronicle_viewer
参考
- DeepZoomフォーマット(DZI)を無料で作りOpenseadragonで表示させる(Qiita, 2024)
- Google MapsライクなOpenSeadragonを使ってみる(Qiita, 2015)
- Introducing Whiiif – Full text searching across image-based collections
- OpenSeadragon: Creating Zooming Images
- OpenSeadragon: DZI Tile Source
- libvips
dzsaveドキュメント - Tesseract Command Line Usage(TSV output)
- MDN: Using Web Workers
- Fuse.js 公式
- Vercel Image Optimization Limits