自動遷移機能を持つIIIF画像座標エディタの開発
概要
今回開発したエディタは、IIIF対応の高解像度画像上で任意の座標を記録・管理するためのWebベースのツールです。URLパラメータで画像を指定でき、様々な研究プロジェクトで利用可能な汎用的な座標記録ツールとして設計されています。

主要技術スタック
- 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をリクエストします。
4. 座標変換システム
OpenSeadragonでは3つの座標系が存在します:
- ピクセル座標: ブラウザ上の表示位置
- ビューポート座標: 正規化された座標 (0〜1)
- 画像座標: 実際の画像ピクセル座標
本ツールでは以下のように変換を実行:
// クリック位置 → 画像座標
const viewportPoint = viewer.viewport.pointFromPixel(event.position);
const imagePoint = viewer.viewport.viewportToImageCoordinates(viewportPoint);
const x = Math.round(imagePoint.x);
const y = Math.round(imagePoint.y);
5. SVGオーバーレイによる動的マーカー表示
ズーム・パン操作に追従するマーカーを実装:
function updateMarkerPositions() {
const markers = svgOverlay.querySelectorAll('g');
markers.forEach(marker => {
const imageX = parseFloat(marker.getAttribute('data-image-x'));
const imageY = parseFloat(marker.getAttribute('data-image-y'));
const imagePoint = new OpenSeadragon.Point(imageX, imageY);
const viewportPoint = viewer.viewport.imageToViewportCoordinates(imagePoint);
const pixelPoint = viewer.viewport.pixelFromPoint(viewportPoint, true);
marker.setAttribute('transform', `translate(${pixelPoint.x}, ${pixelPoint.y})`);
});
}
ビューポート変更イベントで自動更新:
viewer.addHandler('update-viewport', updateMarkerPositions);
viewer.addHandler('resize', updateMarkerPositions);
6. 自動遷移機能
ユーザーの作業効率を最大化するため、次の交点位置を予測して自動的にビューを移動します。
基本アルゴリズム
function predictAndNavigateToNext() {
const lastPoint = points[len - 1];
const secondLastPoint = points[len - 2];
// ベクトル計算
const dx = lastPoint.x - secondLastPoint.x;
const dy = lastPoint.y - secondLastPoint.y;
// 次の予測位置
const predictedX = lastPoint.x + dx;
const predictedY = lastPoint.y + dy;
// ビューを移動
const imagePoint = new OpenSeadragon.Point(predictedX, predictedY);
const viewportPoint = viewer.viewport.imageToViewportCoordinates(imagePoint);
viewer.viewport.panTo(viewportPoint, true);
}
距離変化の検出と警告
異常な移動距離を検出した場合、ユーザーに確認を求めます:
const currentDistance = Math.sqrt(dx * dx + dy * dy);
const prevDistance = Math.sqrt(prevDx * prevDx + prevDy * prevDy);
const distanceRatio = currentDistance / prevDistance;
if (distanceRatio > 1.5 || distanceRatio < 0.67) {
const shouldMove = confirm(
`前回の移動距離と大きく異なります。\n` +
`前回: ${Math.round(prevDistance)}px\n` +
`今回: ${Math.round(currentDistance)}px\n\n` +
`予測位置に移動しますか?`
);
if (!shouldMove) return;
}
これにより、ユーザーの誤クリックやパターン変更を検出できます。
7. データ永続化とバックアップ
localStorage による自動保存
function savePointsToStorage() {
const data = {
imageUrl: imageUrl,
timestamp: new Date().toISOString(),
points: points
};
localStorage.setItem(storageKey, JSON.stringify(data));
}
各画像URLに紐づいた独立したデータ空間で、自動保存が行われます。
エクスポート機能
CSV形式:
number,x,y,memo
1,12345,23456,"重要なポイント"
2,12389,23456,""
JSON形式:
{
"imageUrl": "https://example.com/iiif/image.tif",
"count": 2,
"coordinates": [
{
"id": 1,
"x": 12345,
"y": 23456,
"memo": "重要なポイント"
},
{
"id": 2,
"x": 12389,
"y": 23456
}
]
}
JSONエクスポートには画像URLも含まれ、後からどの画像のデータかを確認できます。
8. 編集モードと閲覧モードの切り替え
function toggleEditMode(enabled) {
editMode = enabled;
// 新規追加の無効化
if (!editMode) {
// canvas-clickイベントで早期リターン
if (!editMode) return;
}
// UIの更新
const clearBtn = document.getElementById('clear-btn');
const importBtn = document.getElementById('import-btn');
clearBtn.disabled = !enabled;
importBtn.disabled = !enabled;
}
閲覧モードでもメモの追加・編集は可能ですが、新規点の追加や削除は無効化されます。
9. ポイント移動機能
順序の入れ替えが必要な場合に対応:
function movePointTo(fromIndex, toIndex) {
// ポイントを抽出
const [movedPoint] = points.splice(fromIndex, 1);
// 新しい位置を計算
let newIndex = toIndex;
if (fromIndex < toIndex) {
newIndex = toIndex;
} else {
newIndex = toIndex + 1;
}
// 挿入
points.splice(newIndex, 0, movedPoint);
// 再描画
redrawMarkers();
}
10. キーボードショートカット
- Delete: 選択中のポイントを削除
- ↑/↓: ポイント間を移動
document.addEventListener('keydown', function(e) {
if (e.key === 'Delete' && selectedPointIndex !== null) {
deletePoint(selectedPointIndex);
}
if (e.key === 'ArrowDown' && selectedPointIndex !== null) {
selectedPointIndex++;
// ビューを移動
const point = points[selectedPointIndex];
const imagePoint = new OpenSeadragon.Point(point.x, point.y);
const viewportPoint = viewer.viewport.imageToViewportCoordinates(imagePoint);
viewer.viewport.panTo(viewportPoint);
}
});
ユーザーインターフェース設計
レスポンシブレイアウト
body {
display: flex;
height: 100vh;
}
#viewer {
flex: 1;
height: 100vh;
}
#sidebar {
width: 400px;
display: flex;
flex-direction: column;
}
#sidebar-content {
flex: 1;
overflow-y: auto;
}
ビューアとサイドバーを固定高さで配置し、スクロール可能なリストエリアを実現。
視覚的フィードバック
- 通常ポイント: 赤い円マーカー + 番号ラベル
- 予測ポイント: 青い破線円 + クロスヘア
- 選択中: 緑色の背景
- 移動モード: オレンジ色の背景
パフォーマンス最適化
1. マーカー更新の最適化
ビューポート変更時にすべてのマーカーを再計算しますが、DOMの再生成は行わず、transform属性のみを更新:
marker.setAttribute('transform', `translate(${pixelPoint.x}, ${pixelPoint.y})`);
2. イベントデリゲーション
個別のマーカーにイベントリスナーを設定せず、親要素でクリックを処理:
headerDiv.onclick = () => {
selectedPointIndex = index;
updatePointsList();
};
データフォーマット
内部データ構造
points = [
{
x: 12345, // 画像上のX座標
y: 23456, // 画像上のY座標
memo: "注釈" // オプションのメモ
},
// ...
]
localStorageフォーマット
{
"imageUrl": "https://img.toyobunko-lab.jp/iiif/...",
"timestamp": "2025-10-29T12:34:56.789Z",
"points": [...]
}
使用例
基本的なワークフロー
- URLパラメータで画像を指定(または、デフォルト画像を使用)
intersection_editor.html?u=https%3A%2F%2Fexample.com%2Fiiif%2Fimage.tif - ページを開くと自動的に保存済みデータを読み込み
- 画像上の座標をクリックして記録
- 自動的に次の予測位置に移動
- 必要に応じてメモを追加
- CSV/JSON形式でエクスポート
複数画像プロジェクトの管理
異なる画像で複数のプロジェクトを並行して作業する場合:
- 各画像専用のURLブックマークを作成
- 各画像のデータは独立してlocalStorageに保存される
- 画像間でデータが混在する心配なし
- JSONエクスポートには画像URLも含まれる
大量データの処理
数百〜数千の座標を記録する場合:
- 自動遷移機能を有効化して効率化
- 定期的にJSON形式でバックアップ
- 誤りがあれば移動機能で順序を修正
- 完了後、CSV形式でエクスポートして分析
セキュリティ考慮事項
XSS対策
メモ入力値をCSVエクスポート時にエスケープ:
const memo = (point.memo || '').replace(/"/g, '""');
csv += `${index + 1},${point.x},${point.y},"${memo}"\n`;
データ検証
JSONインポート時に構造を検証(旧フォーマットとの互換性も保持):
const coordsArray = data.coordinates || data.intersections;
if (!coordsArray || !Array.isArray(coordsArray)) {
alert('無効なJSONフォーマットです。\n"coordinates"または"intersections"配列が必要です。');
return;
}
今後の改善案
- Undo/Redo機能: 操作履歴の実装
- エクスポート形式の拡張: GeoJSON、Allmaps形式への対応
- 協働編集: WebSocketによるリアルタイム共有
- 精度向上: サブピクセル精度の座標記録
- AIアシスト: 交点の自動検出機能
まとめ
本ツールは、URLパラメータによる柔軟な画像指定、画像ごとのデータ分離、IIIF画像の表示機能、自動遷移機能を組み合わせることで、大量の座標データを効率的に記録できる汎用的なシステムを実現しました。
参考リンク
作成日: 2025年10月
バージョン: 1.0
Discussion