ホーム 記事一覧 ブック DH週間トピックス 検索 このサイトについて
English
IIIFマニフェストを用いたテキスト比較ツールの開発

IIIFマニフェストを用いたテキスト比較ツールの開発

はじめに 古典籍のデジタル化が進む中、異なる写本や校訂本のテキストを比較・分析するニーズが高まっています。本稿では、IIIF(International Image Interoperability Framework)マニフェストを活用し、2つの資料の画像とテキストを並べて比較できるWebアプリケーション「Text Comparison Tool」を紹介します。 デモサイト : https://iiif-text.vercel.app/ 背景と課題 デジタルアーカイブで公開されている古典籍には、IIIFマニフェストにテキストアノテーションが付与されているものがあります。しかし、2つの資料のテキストを並べて比較する手軽なツールは多くありません。 例えば、ある作品の校訂本と写本を比較する場合、以下のような作業が必要です: 画像を並べて目視で比較する テキストの差異を一文字ずつ確認する どの程度類似しているかを定量的に把握する これらを1つのツールで実現することを目指しました。 3つの比較モード 本ツールは、3つのモードで資料を比較できます。 1. 画像比較 OpenSeadragonを用いた高精細画像ビューアで、2つの資料の画像を左右に並べて表示します。ズーム・パン・回転に対応し、ページ送りも可能です。 2. テキスト差分(Diff) IIIFマニフェストに含まれるテキストアノテーションを抽出し、文字単位での差分をハイライト表示します。追加箇所は緑色、削除箇所は赤色の取り消し線で表示されます。 3. 編集距離(Levenshtein Distance) レーベンシュタイン距離に基づき、行単位でのテキスト類似度を算出します。結果はネットワークグラフとして可視化され、類似度の高い行同士がエッジで結ばれます。閾値スライダーにより、表示するエッジの最低類似度を調整できます。 技術スタック カテゴリ 技術 フレームワーク Next.js(App Router / Static Export) 言語 TypeScript スタイリング Tailwind CSS v4 UIコンポーネント Radix UI 画像ビューア OpenSeadragon ネットワーク可視化 vis-network 状態管理 Zustand 国際化 next-intl(日本語 / English) 差分検出 diff アーキテクチャ データフロー IIIFマニフェストURL ↓ fetchManifest() — マニフェスト取得・パース ↓ ComparisonValue — 画像URL群、テキスト配列、メタデータ ↓ Zustand Store — アプリケーション状態として保持 ↓ 各比較コンポーネントが消費・描画 fetchManifest()関数がIIIF Presentation API v3のマニフェストをパースし、各キャンバスの画像URLとテキストアノテーションを抽出します。抽出されたデータはZustandストアに格納され、各コンポーネントがリアクティブに参照します。 ...

Mirador 4 で外部マニフェストのウィンドウタイトルだけを差し替える

Mirador 4 で外部マニフェストのウィンドウタイトルだけを差し替える

背景 Mirador は IIIF 対応の画像ビューアで、複数の IIIF マニフェストを並べて比較閲覧できる。複数機関が公開するマニフェストを一画面に並べて表示する際、各ウィンドウのタイトルにはマニフェストの label がそのまま表示される。 しかし、自プロジェクト独自の名称をウィンドウタイトルとして表示したいケースがある。例えば、マニフェストの label が個別の冊次情報を含む長い文字列であるのに対し、資料群を示す短い名称で表示したい場合などである。 制約:マニフェストの中身は変えてはいけない 他機関が公開している IIIF マニフェストを読み込んで表示する以上、その中身を改変して表示することは避けたい。fetch のインターセプトや Mirador 内部状態の書き換えでマニフェスト JSON の label を差し替える方法もあるが、これは実質的にマニフェストの改変にあたる。 変更すべきは Mirador が画面上に描画したウィンドウのタイトル表示(DOM)だけ であり、マニフェストのデータ自体はオリジナルのまま保持したい。 試したアプローチと結果 1. Mirador.actions.receiveManifest による内部状態の書き換え // Mirador の store を監視し、マニフェスト読み込み後に label を書き換え store.subscribe(function () { var state = store.getState(); if (manifests[manifestId] && manifests[manifestId].json && !overridden[manifestId]) { overridden[manifestId] = true; var updatedJson = JSON.parse(JSON.stringify(manifests[manifestId].json)); updatedJson.label = customTitle; store.dispatch(Mirador.actions.receiveManifest(manifestId, updatedJson)); } }); 結果:動作しない。 unpkg から配信される Mirador 4 の UMD ビルドでは Mirador.actions が undefined であり、この API は利用できなかった。 ...

Mirador ビューア埋め込み設定

Mirador ビューア埋め込み設定

IIIF画像の表示に Mirador ビューアを使用する方法について説明します。 参考実装 埋め込み方式は、Stanford University Libraries の Stanford Digital Repository を参考にしています。書誌情報の上部にビューアを埋め込み、メタデータと画像を同一ページで閲覧できるようにしています。 ファイル構成 apps/web/ ├── public/mirador/ │ └── index.html # Mirador ビューア本体 ├── src/components/item/ │ └── MiradorViewer.tsx # 埋め込みコンポーネント └── .env.local # 環境変数設定 URLパラメータ /mirador/index.html は以下のURLパラメータを受け付けます: パラメータ 説明 例 manifest IIIFマニフェストURL(必須)。セミコロン区切りで複数指定可能 https://example.com/iiif/manifest.json embed 埋め込みモード(trueで閉じるボタンと左側メニューを非表示) true theme テーマ(dark または light) dark lang 言語コード(デフォルト: ja) ja, en canvas 初期表示するキャンバスID - annotationState アノテーション表示モード(trueでサイドバー開く) true 使用例 /mirador/index.html?manifest=https://example.com/iiif/manifest.json&embed=true&theme=dark&lang=ja Mirador設定詳細 index.html の構成 DOCTYPE html> html lang="ja"> head> meta charset="utf-8" /> meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> title>Miradortitle> link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" /> head> body> div id="mirador" style="position: absolute; top: 0; bottom: 0; left: 0; right: 0">div> script src="https://unpkg.com/mirador@latest/dist/mirador.min.js">script> script> // 設定スクリプト script> body> html> URLパラメータの解析 var vars = {}; var param = location.search.substring(1).split("&"); for (var i = 0; i param.length; i++) { var keySearch = param[i].search(/=/); var key = ""; if (keySearch != -1) key = param[i].slice(0, keySearch); var val = param[i].slice(param[i].indexOf("=", 0) + 1); if (key != "") vars[key] = decodeURI(val); } ウィンドウ設定 var windows = []; if (vars["manifest"]) { var manifests = vars["manifest"]; var array = manifests.split(";"); // セミコロンで複数マニフェストを分割 for (var i = 0; i array.length; i++) { var manifest = decodeURIComponent(array[i]); var obj = { manifestId: manifest, thumbnailNavigationPosition: "far-right", // サムネイルを右端に表示 }; if (vars["canvas"]) { obj.canvasId = vars["canvas"]; // 初期表示キャンバス } windows.push(obj); } } ウィンドウ動作設定 var windowSettings = { allowClose: true, // 閉じるボタン表示 allowFullscreen: true, // 全画面ボタン表示 } // アノテーションモード if (vars["annotationState"]) { windowSettings.highlightAllAnnotations = true; windowSettings.sideBarOpen = true; windowSettings.defaultSideBarPanel = 'annotations'; } // 埋め込みモード: UIを簡素化 if (vars["embed"] === "true") { windowSettings.allowClose = false; windowSettings.allowMaximize = false; } ワークスペースコントロールパネル 左側のメニュー(マニフェスト追加、ワークスペース管理など)の表示制御: ...

IIIF Georeference ViewerへのLinked Places Format対応

IIIF Georeference ViewerへのLinked Places Format対応

概要 IIIF Georeference Viewerにおいて、地理空間データの相互運用性を向上させるため、Linked Places Format (LPF) に準拠したデータ構造をサポートしました。本記事では、LPFの概要と実装の詳細について説明します。 Linked Places Format (LPF) とは Linked Places Format は、Pelagios Network が策定した地名辞典データの相互運用フォーマットです。GeoJSONを拡張し、Linked Data (JSON-LD) の概念を取り入れることで、異なるデータセット間での場所情報の共有・連携を可能にします。 LPFの特徴 JSON-LD互換 : @id や @context を使用したセマンティックWeb対応 GeoJSON拡張 : 標準的なGeoJSON構造を維持しつつ、メタデータを追加 リンク機能 : 外部データセットへの参照を links 配列で表現 時間情報 : when プロパティによる時間的な情報の記述 公式仕様 GitHub: https://github.com/LinkedPasts/linked-places-format JSON-LD Context: https://raw.githubusercontent.com/LinkedPasts/linked-places/master/linkedplaces-context-v1.1.jsonld 従来のフォーマットとの比較 従来のフォーマット(metadata オブジェクト) { "type": "Feature", "properties": { "resourceCoords": [6690, 7517] }, "geometry": { "type": "Point", "coordinates": [139.7623182, 35.7151233] }, "metadata": { "id": "http://example.org/place/123", "label": "電気実験室", "tags": ["工学部"], "url": "https://maps.app.goo.gl/dJdXXQEA8dWSptgt8", "xywh": "5936,6344,976,1384" } } 従来のフォーマットでは、metadata オブジェクト内に全てのメタデータを格納していました。これはシンプルですが、以下の課題がありました: 標準的なフォーマットではないため、他のツールとの相互運用性が低い Linked Dataとしての活用が困難 外部リソースへのリンクの種類(同一、類似など)を表現できない 新しい推奨フォーマット(LPF準拠) { "type": "Feature", "@id": "https://example.org/places/denki-jikkenshitsu", "properties": { "resourceCoords": [6690, 7517], "title": "電気実験室", "tags": ["工学部"], "xywh": "5936,6344,976,1384" }, "links": [ { "type": "primaryTopicOf", "identifier": "https://maps.app.goo.gl/dJdXXQEA8dWSptgt8" }, { "type": "closeMatch", "identifier": "http://www.wikidata.org/entity/Q123456" } ], "geometry": { "type": "Point", "coordinates": [139.7623182, 35.7151233] } } フォーマット設計の詳細 @id の配置場所 GeoJSON標準 (RFC 7946) とLinked Places Formatでは、識別子の配置場所が異なります: ...

Mirador 4用回転プラグインの開発とnpm公開

Mirador 4用回転プラグインの開発とnpm公開

はじめに IIIFビューアであるMiradorの最新版(Mirador 4)に対応した回転プラグイン「mirador-rotation」を開発し、npmで公開しました。本記事では、プラグインの開発から公開、そして実際に利用するための統合方法について解説します。 背景 Mirador 3からMirador 4へのメジャーアップデートに伴い、以下の変更がありました: React 16 → React 18 Material-UI v4 → MUI v7 その他多数の依存関係の更新 これにより、既存のMirador 3用プラグインはそのままでは動作しなくなりました。 mirador-rotation-pluginの開発 リポジトリ https://github.com/nakamura196/mirador-rotation-plugin 主な機能 画像の回転機能 Mirador 4のプラグインメニューへの統合 npmへの公開 開発したプラグインはnpmで公開しています: npm install mirador-rotation mirador-integrationの更新 公式のmirador-integrationリポジトリを参考に、Mirador 4対応の統合環境を構築しました。 主な変更点 項目 旧 新 Mirador 3.x 4.0.0 React 16.14.0 18.x ビルドツール Webpack Parcel UI Material-UI v4 MUI v7 package.json 最小限の依存関係で構成しています: { "dependencies": { "mirador": "^4.0.0", "mirador-rotation": "^4.0.0", "parcel": "^2.0.0", "react": "^18.0.0", "react-dom": "^18.0.0" } } 外部から利用するための工夫 問題 公式のmirador-integrationをそのままビルドすると、ESモジュール形式で出力されます。しかし、別のHTMLページから<script>タグで読み込んで使いたい場合、グローバル変数としてMiradorが定義されないという問題がありました。 script src="mirador.js">script> script> Mirador.viewer({...}); // Mirador is not defined script> 解決策 1. ライブラリ用エントリーポイントの作成 index.jsでグローバル変数として明示的にエクスポートします: ...

静的サイトでIIIF Content Search APIを実現する - Service Workerによるクライアントサイド検索

静的サイトでIIIF Content Search APIを実現する - Service Workerによるクライアントサイド検索

はじめに IIIF (International Image Interoperability Framework) は、デジタルアーカイブや美術館のコレクションで広く使われている画像配信の国際規格です。IIIF Content Search API を使うと、マニフェスト内のアノテーション(注釈やタグ)を検索できます。 しかし、IIIF Content Search API は通常、サーバーサイドでの実装が前提となっており、静的サイト(GitHub Pages、Vercel、Netlify など)では実現が難しいとされてきました。 本記事では、Service Worker を使ってクライアントサイドで IIIF Content Search API を実装する方法 を紹介します。この手法により、静的サイトでも Mirador などの IIIF ビューアで検索機能を利用できるようになります。 課題 従来のIIIF Search APIの仕組み [Mirador] → GET /search?q=keyword → [サーバー] → 検索処理 → JSON応答 IIIF Content Search API は、クエリパラメータ(?q=検索語)を受け取り、検索結果を JSON で返すエンドポイントを必要とします。これは動的なサーバー処理を前提としています。 静的サイトの制約 静的サイトでは: クエリパラメータに応じた動的なレスポンスを返せない サーバーサイドの検索処理を実行できない 静的 JSON ファイルしか配信できない 解決策:Service Worker によるリクエストインターセプト Service Worker は、ブラウザとネットワークの間に位置するプロキシとして機能します。これを活用して、検索リクエストをインターセプトし、クライアントサイドで検索処理を行います。 アーキテクチャ [Mirador] │ │ GET /iiif/site/search/index.json?q=keyword ↓ [Service Worker] ← インターセプト │ ├─ 静的な index.json を fetch(初回のみ) │ ├─ JavaScript で検索を実行 │ └─ IIIF Content Search API 形式で応答 ↓ [Mirador] ← 検索結果を表示 実装 1. 検索インデックスの生成(ビルド時) まず、アノテーションデータから検索インデックスを生成します。 ...

IIIF画像をWeb Tile Map Serviceで配信する

IIIF画像をWeb Tile Map Serviceで配信する

IIIF Georeference Extension JSONからXYZタイルを生成し、TileServer GLで配信、MapLibre GL JSで表示するまでの手順をまとめます。 OSM上に東京大学鳥瞰図をオーバーレイ表示 概要 IIIF Georeference JSON │ ▼ ┌───────────────────────┐ │ iiif-georef-tiles │ │ (XYZタイル生成) │ └───────────────────────┘ │ ▼ ┌───────────────────────┐ │ mb-util │ │ (mbtiles変換) │ └───────────────────────┘ │ ▼ ┌───────────────────────┐ │ TileServer GL │ │ (タイル配信) │ └───────────────────────┘ │ ▼ ┌───────────────────────┐ │ MapLibre GL JS │ │ (地図表示) │ └───────────────────────┘ 必要環境 Docker / Docker Compose Python 3.x GDAL (gdal_translate, gdalwarp, gdal2tiles.py) Pillow (pip3 install pillow) mb-util GDALのインストール # macOS (Homebrew) brew install gdal # Ubuntu/Debian sudo apt install gdal-bin python3-gdal mb-utilのインストール pip3 install mbutil 1. プロジェクト構成 wtms/ ├── docker-compose.yml ├── data/ # mbtilesファイル ├── styles/ # カスタムスタイル(オプション) ├── frontend/ # MapLibreビューア └── docs/ 2. Docker Compose設定 docker-compose.yml: ...

IIIF Georeference to XYZ Tiles

IIIF Georeference to XYZ Tiles

IIIF Georeference Extension JSONからXYZタイルを生成し、MapLibre GL JSで表示するツール。 リポジトリ : https://github.com/nakamura196/iiif-georef-tiles GitHub Pages : https://nakamura196.github.io/iiif-georef-tiles/ 必要環境 Python 3.x GDAL (gdal_translate, gdalwarp, gdal2tiles.py) GDALのインストール # macOS (Homebrew) brew install gdal # Ubuntu/Debian sudo apt install gdal-bin python3-gdal 使用方法 python3 scripts/iiif_georef_to_tiles.py <IIIF_GEOREF_JSON_URL> 例 python3 scripts/iiif_georef_to_tiles.py https://nakamura196.github.io/iiif_geo/canvas.json オプション オプション デフォルト 説明 --scale 0.25 画像の縮小率 --zoom 14-18 タイルのズームレベル範囲 --output-dir docs 出力ディレクトリ --name tiles タイルフォルダ名 --work-dir work 作業用ディレクトリ --keep-work - 作業用ファイルを削除しない 処理の流れ IIIF Georeference JSON │ ▼ ┌───────────────────────┐ │ 1. JSONを取得 │ │ (URLからfetch) │ └───────────────────────┘ │ ▼ ┌───────────────────────┐ │ 2. 画像をダウンロード │ │ (IIIF Image API) │ └───────────────────────┘ │ ▼ ┌───────────────────────┐ │ 3. GCPを埋め込み │ │ (gdal_translate) │ └───────────────────────┘ │ ▼ ┌───────────────────────┐ │ 4. 座標変換 │ │ (gdalwarp) │ └───────────────────────┘ │ ▼ ┌───────────────────────┐ │ 5. タイル生成 │ │ (gdal2tiles.py) │ └───────────────────────┘ │ ▼ ┌───────────────────────┐ │ 6. HTMLビューア生成 │ │ (MapLibre GL JS) │ └───────────────────────┘ 変換結果 元画像 地理参照後 出力ファイル docs/ ├── index.html # MapLibre GL JSビューア ├── source.json # 元のIIIF Georeference JSON └── tiles/ # XYZタイル ├── 14/ ├── 15/ ├── 16/ ├── 17/ └── 18/ ローカルで確認 cd docs && python3 -m http.server 8000 # http://localhost:8000/ を開く IIIF Georeference Extension IIIF Georeference Extensionは、IIIF画像に地理参照情報を付与するための拡張仕様です。 ...

GLBファイルのDraco圧縮 - 87%のサイズ削減と精度への影響

GLBファイルのDraco圧縮 - 87%のサイズ削減と精度への影響

3DモデルをWebで配信する際、ファイルサイズは重要な課題です。本記事では、Draco圧縮 を使ってGLBファイルを87%削減した事例と、圧縮時の注意点(特にUV座標)について解説します。 https://3dtiles-viewer.vercel.app/glb-viewer.html 使用データ モデル : Rotunde Brunnen(噴水のある円形建築物) 出典 : Sketchfab 形式 : GLB (glTF 2.0 Binary) Draco圧縮とは DracoはGoogleが開発したオープンソースの3Dメッシュ圧縮ライブラリです。glTF 2.0ではKHR_draco_mesh_compression拡張として標準サポートされています。 圧縮の仕組み 量子化(Quantization) : 頂点座標やUV座標を指定ビット数に丸める 予測符号化 : 隣接頂点との差分を予測して符号化 エントロピー符号化 : 予測誤差を効率的に圧縮 圧縮コマンド # gltf-transformを使用 npx gltf-transform draco input.glb output-draco.glb # オプション付き(高品質設定) npx gltf-transform draco input.glb output-draco.glb \ --quantize-position 14 \ --quantize-normal 10 \ --quantize-texcoord 12 圧縮結果の比較 ファイルサイズ ファイル サイズ 削減率 rotunde-brunnen.glb(元) 94.7 MB - rotunde-brunnen-draco.glb 12.5 MB 87%削減 メッシュ構造 項目 元ファイル Draco圧縮後 メッシュ数 38 2(統合) 三角形数 約175万 約167万 テクスチャ 1024x1024 PNG 同一 バウンディングボックス ほぼ同一 ほぼ同一 精度 視覚的な精度低下はありません 。 ...

300万点超の点群データをブラウザで快適に表示する - Potree LODビューアの構築

300万点超の点群データをブラウザで快適に表示する - Potree LODビューアの構築

大規模な点群データ(LiDAR/LAZ)をWebブラウザで表示しようとすると、メモリ不足でクラッシュしてしまうことがあります。本記事では、Potree のLOD(Level of Detail)技術を使って、数百万点の点群をストレスなく表示する方法を紹介します。 https://3dtiles-viewer.vercel.app/potree-lod-viewer.html 使用データ データ名 : Utah State Capitol(ユタ州議事堂) 出典 : OpenTopography ダウンロードURL : https://object.cloud.sdsc.edu/v1/AUTH_opentopography/www/education/MatlabTopo/Utah_state_capitol.laz ファイルサイズ : 15MB(LAZ圧縮) 点数 : 3,481,512点 位置 : Salt Lake City, Utah, USA 課題 このデータをそのままThree.jsなどで読み込もうとすると、ブラウザがフリーズする可能性があります。 解決策:Potree Potreeは、大規模点群データのためのWebGLベースのビューアです。**LOD(Level of Detail)**により、カメラに近い部分は詳細に、遠い部分は粗く表示することで、数十億点のデータでもスムーズに動作します。 仕組み 点群をオクトリー構造 で空間分割 各ノードに異なる詳細度のデータを格納 視点に応じて必要なノードのみ動的に読み込み 手順 1. LAZファイルのダウンロード curl -L -o utah_capitol.laz \ "https://object.cloud.sdsc.edu/v1/AUTH_opentopography/www/education/MatlabTopo/Utah_state_capitol.laz" 2. LAZからPotree形式への変換 PotreeConverterをDockerで実行します。 # Dockerイメージの取得 docker pull synth3d/potreeconverter # 変換実行 docker run --rm \ -v $(pwd):/data \ -v $(pwd)/output:/output \ synth3d/potreeconverter \ PotreeConverter /data/utah_capitol.laz -o /output/utah_capitol 出力結果 : ...

mirador-annotations を Mirador 4.x へ移行した記録

mirador-annotations を Mirador 4.x へ移行した記録

背景 mirador-annotations は、IIIF ビューア Mirador にアノテーション機能を追加するプラグインです。 従来のプロジェクトは以下の構成でした: ビルドツール : nwb (Create React App ベース) UI ライブラリ : Material-UI v4 Mirador : 3.x React : 17.x しかし、以下の問題が発生していました: nwb のメンテナンス停止 - nwb は長期間更新されておらず、依存関係の競合が頻発 npm install の失敗 - 古い依存関係により、新しい環境でのセットアップが困難に セキュリティ脆弱性 - 古いパッケージに多数の脆弱性警告 これらの問題を解決するため、以下への移行を決定しました: ビルドツール : Vite UI ライブラリ : MUI v7 Mirador : 4.x React : 18.x 移行作業の概要 1. ビルドツールの移行 (nwb → Vite) nwb の設定ファイルを削除し、vite.config.js を新規作成しました。 主なポイント: // vite.config.js export default defineConfig(({ mode }) => { const env = loadEnv(mode, process.cwd(), ''); return { // draft-js が global を参照するため define: { global: 'globalThis', }, // 重複パッケージの解決 resolve: { dedupe: [ '@emotion/react', '@emotion/styled', 'react', 'react-dom', ], }, }; }); 2. Material-UI の移行 (v4 → v7) @material-ui/* を @mui/* に変更 makeStyles を sx prop に置き換え Grid コンポーネントの API 変更に対応 (item と xs props が size に統合) // 変更前 (MUI v4) Grid item xs={12}> // 変更後 (MUI v7) Grid size={12}> 3. Mirador 4.x への対応 Mirador 4.x では、アクションやセレクターのインポート方法が変更されました: ...

mirador-rotation-plugin 機能拡張

mirador-rotation-plugin 機能拡張

概要 mirador-rotation-pluginに以下の機能を追加しました: 90度単位の回転ボタン URLパラメータによるマニフェスト・回転角度の指定 UIの改善(リセットボタンのアイコン変更) ヘルプ機能(使い方を説明するダイアログ) 新機能の詳細 1. 90度単位の回転ボタン 従来は1度単位のスライダーのみでしたが、90度単位で素早く回転できるボタンを追加しました。 実装内容 src/plugins/MiradorRotation.js に以下の変更を加えました: import RotateLeftIcon from '@mui/icons-material/RotateLeft'; import RotateRightIcon from '@mui/icons-material/RotateRight'; // 90度回転のハンドラー const handleRotate90 = (direction) => { const newRotation = rotation + (direction * 90); updateViewport(windowId, { rotation: newRotation }); }; UIには2つのボタンを追加: 左回転ボタン : 反時計回りに90度回転 右回転ボタン : 時計回りに90度回転 翻訳対応 src/translations.js に英語・日本語の翻訳を追加: { en: { rotateLeft: 'Rotate 90° left', rotateRight: 'Rotate 90° right', }, ja: { rotateLeft: '左に90度回転', rotateRight: '右に90度回転', }, } 2. URLパラメータ対応 デモページでURLパラメータからマニフェストと回転角度を指定できるようになりました。 対応パラメータ パラメータ 説明 区切り文字 manifest IIIFマニフェストURL ;(セミコロン) rotation 初期回転角度(度) ;(セミコロン) デモページURL https://nakamura196.github.io/mirador-rotation-plugin/ 使用例 # 単一マニフェスト https://nakamura196.github.io/mirador-rotation-plugin/?manifest=https://example.com/manifest.json&rotation=180 # 複数マニフェスト(同じ回転角度) https://nakamura196.github.io/mirador-rotation-plugin/?manifest=url1;url2&rotation=90 # 複数マニフェスト(異なる回転角度) https://nakamura196.github.io/mirador-rotation-plugin/?manifest=url1;url2&rotation=90;180 実装内容 demo/src/index.js の主な変更: ...

IIIF Manifestから各巻の冒頭ページを抽出するツールを作成しました

IIIF Manifestから各巻の冒頭ページを抽出するツールを作成しました

はじめに IIIF(International Image Interoperability Framework)を利用したデジタルアーカイブでは、複数巻や複数章で構成される資料を1つのManifestにまとめることがあります。このような場合、各巻・各章の冒頭ページへのリンクを作成したいというニーズがあります。 今回、IIIF Manifestから各巻(range/structure)のlabel と最初のCanvas URL を抽出するシンプルなWebツールを作成しました。 ツールURL : https://nakamura196.github.io/iiif-manifest-extractor/ GitHub : https://github.com/nakamura196/iiif-manifest-extractor 機能 複数のManifest URLを一括処理(1行に1つのURL) 各巻・各章のlabelと最初のCanvas URLを一覧表示 CSV/JSON形式でのエクスポート 処理進捗のリアルタイム表示 使い方 ツールを開く Manifest URLをテキストエリアに入力(複数行可) 「抽出」ボタンをクリック 結果が表形式で表示される 必要に応じてCSV/JSONでダウンロード サンプル 以下のManifest URLで動作を確認できます。複数URLを入力することで、一括処理の動作も確認できます。 国立国会図書館デジタルコレクション「校異源氏物語」: https://dl.ndl.go.jp/api/iiif/3437686/manifest.json https://dl.ndl.go.jp/api/iiif/3437687/manifest.json これらのManifestは源氏物語の各帖(きりつほ、ははきゝ、うつせみ、わかむらさき…など)がstructuresに定義されており、各帖の冒頭ページを抽出できます。 2つのManifest URLを入力することで、複数のManifestを一括で処理し、結果をまとめてCSV出力できることを確認できます。 技術的な仕組み IIIF Presentation API v2のstructures IIIF Presentation API v2では、structuresプロパティを使って論理的な構造(目次)を定義できます。 { "structures": [ { "@type": "sc:Range", "label": "目次", "ranges": ["range1", "range2", ...] }, { "@id": "range1", "@type": "sc:Range", "label": "きりつほ", "canvases": [ "https://example.com/canvas/p1", "https://example.com/canvas/p2", ... ] } ] } 本ツールでは、structures内の各rangeから: label(巻名・章名など) canvases配列の最初の要素(冒頭ページのCanvas URL) を抽出しています。 ...

TEI ODDによるIIIF対応ファクシミリ記述の制約設計

TEI ODDによるIIIF対応ファクシミリ記述の制約設計

はじめに TEI(Text Encoding Initiative)でデジタル画像のメタデータを記述する際、facsimile要素を使用します。特にIIIF(International Image Interoperability Framework)対応のデジタルアーカイブでは、マニフェストやキャンバス、Image APIへの参照を適切に記述することが重要です。 本記事では、ODD(One Document Does it all)を使用して、ファクシミリ記述に必要な制約をスキーマとして定義する方法を紹介します。 準拠するガイドライン 本ODDは、日本語TEIガイドラインで紹介されている「IIIF画像とのリンク」仕様をベースにしています: IIIF画像とのリンク(2024年度版)- TEI-EAJ このガイドラインに準拠したデータを作成することで、TEI Viewer for EAJでの画像表示が可能になります。TEI Viewerは、TEIテキストとIIIF画像を連携して表示できるビューアであり、facsimile要素の情報を利用してテキストと画像の対応付けを行います。 設計目標 以下の要件を満たすスキーマを設計しました: 必須情報の明確化 : 画像の座標情報や識別子など、最低限必要な情報を必須属性として定義 IIIF対応 : マニフェスト、キャンバス、Image APIへの参照をオプショナルに記述可能 再利用性 : 独立したODDファイルとして、複数プロジェクトで共有可能 ビューア互換性 : TEI Viewer for EAJでの表示に必要な情報を確実に記録 最小限の記述例 <facsimile sameAs="https://example.org/iiif/manifest.json"> <surface ulx="0" uly="0" lrx="5600" lry="4000" xml:id="p1"> <graphic sameAs="https://example.org/image/001.tif"/> </surface> </facsimile> 完全な記述例(IIIF参照を含む) <facsimile sameAs="https://example.org/iiif/manifest.json"> <surface ulx="0" uly="0" lrx="5600" lry="4000" sameAs="https://example.org/canvas/p1" xml:id="p1"> <graphic url="https://example.org/image/001.tif/full/full/0/default.jpg" sameAs="https://example.org/image/001.tif"/> </surface> </facsimile> ODD定義の解説 1. facsimile要素 <elementSpec ident="facsimile" mode="change"> <desc>ファクシミリ画像情報。IIIFマニフェストへの参照を含む。</desc> <classes mode="replace"/> <content> <elementRef key="surface" minOccurs="1" maxOccurs="unbounded"/> </content> <attList mode="replace"> <attDef ident="sameAs" mode="replace" usage="opt"> <desc>IIIFマニフェストへの参照URL</desc> <datatype> <dataRef key="teidata.pointer"/> </datatype> </attDef> </attList> </elementSpec> ポイント : ...

Mirador 4で任意の領域をハイライト表示する方法

Mirador 4で任意の領域をハイライト表示する方法

はじめに IIIFビューアのMiradorには検索機能があり、IIIF Search APIに対応したマニフェストでは検索結果をハイライト表示できます。しかし、Search APIに非対応のマニフェストでも、任意の領域をハイライト表示したいケースがあります。 本記事では、Miradorの内部APIを利用して、外部データソースからのアノテーション情報を基にハイライト表示を実現する方法を紹介します。 デモ Highlight Generator Form - フォームからハイライトを生成 ユースケース 独自のOCRシステムで抽出したテキスト領域のハイライト 機械学習で検出したオブジェクトの領域表示 外部データベースに保存されたアノテーションの可視化 Search API非対応のIIIFサーバーでの検索結果表示 実装方法 基本的な仕組み Miradorは内部でReduxを使用しており、receiveSearchアクションを通じて検索結果を登録できます。このアクションにIIIF Search API形式のJSONを渡すことで、任意のデータソースからのハイライトを表示できます。 必要な情報 ハイライトを表示するために必要な情報は以下の3つです: キャンバスURI - ハイライトを表示するページのURI 座標(xywh) - ハイライト領域の位置とサイズ(x, y, width, height) テキスト - ハイライトに関連付けるテキスト(検索パネルに表示される) サンプルコード 以下は、国立国会図書館デジタルコレクションの源氏物語で「いつれの御時にか…」の冒頭部分をハイライト表示するサンプルです。 <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Mirador Custom Highlight Sample</title> <style> body { margin: 0; padding: 0; } #mirador-viewer { width: 100%; height: 100vh; } </style> </head> <body> <div id="mirador-viewer"></div> <script src="https://unpkg.com/mirador@4.0.0-alpha.15/dist/mirador.min.js"></script> <script> // 設定パラメータ const config = { manifestUrl: 'https://dl.ndl.go.jp/api/iiif/3437686/manifest.json', canvasId: 'https://dl.ndl.go.jp/api/iiif/3437686/canvas/22', highlights: [ { xywh: '3095,694,97,2051', text: 'いつれの御時にか女御更衣あまたさふらひ給けるなかにいとやむことなきゝは', }, ], }; // Miradorを初期化 const miradorViewer = Mirador.viewer({ id: 'mirador-viewer', selectedTheme: 'light', language: 'ja', windows: [{ id: 'window-1', manifestId: config.manifestUrl, canvasId: config.canvasId, thumbnailNavigationPosition: 'far-right', }], window: { allowFullscreen: true, allowClose: false, allowMaximize: false, sideBarOpen: true, }, workspaceControlPanel: { enabled: false, }, }); // ハイライトを追加する関数 function addHighlights(viewer, canvasId, highlights) { // IIIF Search API形式のレスポンスを構築 const searchResponse = { '@context': 'http://iiif.io/api/search/1/context.json', '@id': canvasId + '/search', '@type': 'sc:AnnotationList', within: { '@type': 'sc:Layer', total: highlights.length, }, resources: highlights.map((highlight, index) => ({ '@id': canvasId + '/highlight-' + index, '@type': 'oa:Annotation', motivation: 'sc:painting', resource: { '@type': 'cnt:ContentAsText', chars: highlight.text, }, on: canvasId + '#xywh=' + highlight.xywh, })), }; // 検索パネルを右側に追加 const addAction = Mirador.addCompanionWindow('window-1', { content: 'search', position: 'right', }); viewer.store.dispatch(addAction); // companionWindowIdを取得 const state = viewer.store.getState(); const searchCompanionWindowId = Object.keys(state.companionWindows).find( id => state.companionWindows[id].content === 'search' ); if (searchCompanionWindowId) { // 検索結果を登録 const searchAction = Mirador.receiveSearch( 'window-1', searchCompanionWindowId, canvasId + '/search', searchResponse ); viewer.store.dispatch(searchAction); } } // マニフェストの読み込み完了を監視してハイライトを追加 let highlightAdded = false; const unsubscribe = miradorViewer.store.subscribe(() => { if (highlightAdded) return; const state = miradorViewer.store.getState(); const manifests = state.manifests || {}; const manifest = manifests[config.manifestUrl]; // マニフェストが読み込み完了したらハイライトを追加 if (manifest && !manifest.isFetching && manifest.json) { highlightAdded = true; unsubscribe(); addHighlights(miradorViewer, config.canvasId, config.highlights); } }); </script> </body> </html> コードの解説 1. 設定パラメータ const config = { manifestUrl: 'https://dl.ndl.go.jp/api/iiif/3437686/manifest.json', canvasId: 'https://dl.ndl.go.jp/api/iiif/3437686/canvas/22', highlights: [ { xywh: '3095,694,97,2051', text: 'いつれの御時にか...', }, ], }; manifestUrl: IIIFマニフェストのURL canvasId: ハイライトを表示するキャンバスのURI highlights: ハイライト情報の配列。複数のハイライトを追加可能 2. IIIF Search API形式のレスポンス構築 const searchResponse = { '@context': 'http://iiif.io/api/search/1/context.json', '@type': 'sc:AnnotationList', resources: highlights.map((highlight, index) => ({ '@type': 'oa:Annotation', motivation: 'sc:painting', resource: { '@type': 'cnt:ContentAsText', chars: highlight.text, }, on: canvasId + '#xywh=' + highlight.xywh, })), }; ポイントは on プロパティで、キャンバスURI#xywh=x,y,width,height の形式でハイライト領域を指定します。 ...

Mirador 4でキャンバス指定と検索語ハイライトを同時に実現する方法

Mirador 4でキャンバス指定と検索語ハイライトを同時に実現する方法

はじめに IIIF(International Image Interoperability Framework)ビューアとして広く使われているMiradorで、以下の要件を満たす実装を行いました: URLパラメータで指定したキャンバス(ページ)を初期表示する 指定したキャンバス内の検索語をハイライト表示する 本記事では、この要件を実現するためのアプローチと実装方法を共有します。 アプローチの検討 defaultSearchQueryオプション Mirador 4では、ウィンドウ設定に defaultSearchQuery オプションを指定することで、初期化時に自動的に検索を実行できます: const miradorViewer = Mirador.viewer({ windows: [{ manifestId: manifestUrl, canvasId: canvasId, defaultSearchQuery: '検索語', }], }); このオプションは検索を自動実行する便利な機能ですが、今回の要件では以下の点を考慮する必要がありました: ページ遷移の制御 - 検索実行時、最初のヒットのページに自動遷移する仕様のため、canvasId で指定したページに留まりたい場合は追加の制御が必要 検索完了タイミングの把握 - 非同期で検索が実行されるため、完了後に処理を行いたい場合はRedux状態の監視が必要 より直接的なアプローチ 検討の結果、receiveSearch アクションを直接使用するアプローチを採用しました。このアプローチでは: Search APIを直接呼び出して、指定キャンバスに該当するヒットのみを取得 IIIF Search API形式のレスポンスを構築 receiveSearch アクションで検索結果としてMiradorに登録 これにより、指定キャンバスの表示を維持しながら、Miradorの検索ハイライト機能をそのまま活用できます。 実装 1. Search APIからヒットを取得する関数 const getSearchHitsForCanvas = async ( manifestUrl: string, canvasId: string, query: string ): Promise<{ id: string; chars: string; xywh: string }[]> => { try { // manifestからsearch serviceのURLを取得 const manifestResponse = await fetch(manifestUrl); if (!manifestResponse.ok) return []; const manifest = await manifestResponse.json(); // IIIF Search API serviceを探す const services = manifest.service || []; const searchService = (Array.isArray(services) ? services : [services]).find( (s: { profile?: string }) => s.profile?.includes('search') ); if (!searchService) return []; const searchBaseUrl = searchService['@id'] || searchService.id; const searchUrl = `${searchBaseUrl}?q=${encodeURIComponent(query)}`; const response = await fetch(searchUrl); if (!response.ok) return []; const data = await response.json(); const resources = data.resources || []; // 指定キャンバスに該当するヒットのみを抽出 const hits: { id: string; chars: string; xywh: string }[] = []; for (const resource of resources) { const [resourceCanvas, fragment] = resource.on.split('#'); if (resourceCanvas === canvasId && fragment) { hits.push({ id: resource['@id'] || resource.id, chars: resource.resource?.chars || query, xywh: fragment.replace('xywh=', ''), }); } } return hits; } catch (error) { console.error('Failed to fetch search hits:', error); return []; } }; この実装では、manifestのserviceプロパティからIIIF Search APIのエンドポイントを動的に取得しています。これにより、任意のIIIF準拠サーバーで動作します。 ...

vipsによるピラミダルタイルTIFF作成と圧縮方式の比較

vipsによるピラミダルタイルTIFF作成と圧縮方式の比較

はじめに 高解像度画像をWeb上で快適に閲覧するためには、ピラミダル構造(複数解像度)とタイル分割が不可欠です。本記事では、vipsを使用してJPEG2000画像からピラミダルタイルTIFFを作成し、各圧縮方式のファイルサイズを比較検証しました。 検証環境 vips 8.17.3 macOS (darwin) 元画像: 764029-1.jp2 (274MB) 出典: 国立公文書館デジタルアーカイブ vipsコマンド JPEG圧縮(非可逆) # 品質100(ほぼ無劣化) vips tiffsave input.jp2 output_q100.tif --tile --pyramid --compression=jpeg --Q=100 # 品質75(バランス型) vips tiffsave input.jp2 output_q75.tif --tile --pyramid --compression=jpeg --Q=75 # 品質25(高圧縮) vips tiffsave input.jp2 output_q25.tif --tile --pyramid --compression=jpeg --Q=25 ロスレス圧縮 # Deflate圧縮(zlib) vips tiffsave input.jp2 output_deflate.tif --tile --pyramid --compression=deflate # LZW圧縮 vips tiffsave input.jp2 output_lzw.tif --tile --pyramid --compression=lzw # 無圧縮(4GB超の場合はBigTIFF形式が必要) vips tiffsave input.jp2 output_none.tif --tile --pyramid --compression=none --bigtiff 検証結果 ファイル 圧縮方式 サイズ 元ファイル比 備考 元ファイル JPEG2000 274MB - 入力 q25.tif JPEG Q=25 57MB 0.21x 非可逆・高圧縮 q75.tif JPEG Q=75 167MB 0.61x 非可逆・バランス q100.tif JPEG Q=100 2.4GB 8.8x 非可逆・高品質 deflate.tif Deflate 2.8GB 10.2x ロスレス lzw.tif LZW 3.2GB 11.7x ロスレス none.tif 無圧縮 4.3GB 15.7x ロスレス 画質比較 JPEG圧縮の品質による違いを視覚的に比較しました(左から Q=25, Q=75, Q=100)。 ...

アノテーション表示のパフォーマンス改善

アノテーション表示のパフォーマンス改善

概要 3Dビューワーでアノテーションが多数ある場合、背面判定(Raycast)処理がパフォーマンスのボトルネックになります。本ドキュメントでは、採用した改善手法について説明します。 問題 アノテーションの背面判定には、各アノテーションに対してRaycast(光線とメッシュの衝突判定)を実行する必要があります。この処理は以下の理由で重くなります: メッシュの全頂点との衝突判定が必要 アノテーション数に比例して計算量が増加 毎フレーム実行すると60FPSの維持が困難 解決策: Idle時のみRaycast実行 カメラが停止した時のみRaycast処理を実行 する方式を採用しました。 動作フロー カメラ移動中 → Raycast処理をスキップ(軽量) ↓ カメラ停止検出 ↓ 30フレーム待機(約0.5秒、安定化) ↓ 1回だけRaycast実行 ↓ 次にカメラが動くまで再計算しない 実装詳細 // パフォーマンス設定 const CAMERA_MOVE_THRESHOLD = 0.01; // カメラ移動の閾値(スクロール時の微小な動きを無視) const IDLE_FRAMES_BEFORE_RAYCAST = 30; // 停止後このフレーム数待ってからRaycast(約0.5秒 @ 60fps) useFrame(() => { // カメラの移動量をチェック const cameraMoved = camera.position.distanceTo(prevCameraPosition) > CAMERA_MOVE_THRESHOLD; prevCameraPosition.copy(camera.position); if (cameraMoved) { // カメラが動いている間はカウンタをリセット idleFrameCountRef.current = 0; needsRaycastRef.current = true; return; // Raycast処理をスキップ } // カメラが停止している idleFrameCountRef.current++; // 停止後、一定フレーム待ってからRaycast実行(1回のみ) if (!needsRaycastRef.current) return; if (idleFrameCountRef.current < IDLE_FRAMES_BEFORE_RAYCAST) return; // Raycast実行(1回のみ) needsRaycastRef.current = false; // ... Raycast処理 ... }); 2段階の判定処理 Raycast処理自体も最適化しています: ...

傾いた文字のアノテーションとIIIF画像切り出し

傾いた文字のアノテーションとIIIF画像切り出し

はじめに 古地図や古文書には、様々な方向に傾いた文字が含まれています。本ツールでは、多角形(ポリゴン)アノテーションを使用して傾いた文字を正確にマークアップし、その傾き情報を活用してIIIF Image APIで回転補正された画像を取得できます。 傾き計算の仕組み 頂点の順序ルール 多角形アノテーションを作成する際、以下の順序で頂点を指定します: 左上 → 2. 左下 → 3. 右下 → 4. 右上 この反時計回りの順序を守ることで、傾き角度を一意に計算できます。 左上(1) ─────────── 右上(4) │ │ │ 文字領域 │ │ │ 左下(2) ─────────── 右下(3) 以下のデモ動画も参考にしてください。 https://youtu.be/P9srTeynXuk?si=mJO1yu3IhR0QFV-2 角度の計算方法 上辺(左上→右上)のベクトルから傾き角度を計算します: // SVGパスから頂点を抽出 // M x1,y1 L x2,y2 L x3,y3 L x4,y4 Z // 頂点0(左上), 頂点1(左下), 頂点2(右下), 頂点3(右上) const dx = 右上.x - 左上.x; // 頂点3.x - 頂点0.x const dy = 右上.y - 左上.y; // 頂点3.y - 頂点0.y const angle = Math.atan2(dy, dx) * (180 / Math.PI); // IIIFの回転パラメータ(時計回りが正) let iiifRotation = -angle; if (iiifRotation 0) iiifRotation += 360; 実際の使用例 例1: 横書きでほぼ水平な文字 アノテーションデータ: ...

自動遷移機能を持つIIIF画像座標エディタの開発

自動遷移機能を持つIIIF画像座標エディタの開発

概要 今回開発したエディタは、IIIF対応の高解像度画像上で任意の座標を記録・管理するためのWebベースのツールです。URLパラメータで画像を指定でき、様々な研究プロジェクトで利用可能な汎用的な座標記録ツールとして設計されています。 https://youtu.be/UqPo5Xrkin8 主要技術スタック OpenSeadragon : IIIF画像ビューアライブラリ (v4.1) SVGオーバーレイ : マーカー表示用 localStorage : データの永続化 Vanilla JavaScript : フレームワークレス実装 技術的特徴 1. URLパラメータによる画像指定 ツールの最大の特徴は、URLパラメータで任意のIIIF画像を指定できることです: function getImageUrlFromQuery() { const urlParams = new URLSearchParams(window.location.search); const urlParam = urlParams.get('u'); if (urlParam) { try { return decodeURIComponent(urlParam); } catch (e) { console.error('Error decoding URL parameter:', e); alert('URLパラメータのデコードに失敗しました。デフォルト画像を使用します。'); } } // Default image URL return 'https://img.toyobunko-lab.jp/iiif/premodern_chinese/suikeichuzu/Suikeichuuzu_grid_l.tif'; } const imageUrl = getImageUrlFromQuery(); 使用例: intersection_editor.html?u=https%3A%2F%2Fexample.com%2Fiiif%2Fimage.tif URLエンコードされた画像URLを?u=パラメータで渡すだけで、任意の画像を開けます。 2. 画像URLごとのデータ分離 localStorageのキーに画像URLを含めることで、画像ごとに独立したデータを管理: const imageUrl = getImageUrlFromQuery(); const storageKey = `intersection_points_${btoa(imageUrl).substring(0, 50)}`; 画像URLをBase64エンコードしてキーの一部とすることで、複数の画像プロジェクトを同時に管理できます。 3. IIIF画像の自動読み込み 画像URLから自動的にIIIF info.json URLを生成: function getIIIFInfoUrl(imageUrl) { if (imageUrl.endsWith('info.json')) { return imageUrl; } let baseUrl = imageUrl.replace(/\.(jpg|jpeg|png|tif|tiff)$/i, ''); return `${baseUrl}/info.json`; } const viewer = OpenSeadragon({ id: "viewer", tileSources: imageUrl.startsWith('http') ? getIIIFInfoUrl(imageUrl) : imageUrl, showNavigationControl: true, showNavigator: true }); .tifや.jpgなどの拡張子を自動的に処理し、IIIF Image API 2.0に準拠したinfo.jsonをリクエストします。 ...