自動遷移機能を持つ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をリクエストします。

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": [...]
}

使用例

基本的なワークフロー

  1. URLパラメータで画像を指定(または、デフォルト画像を使用)
    intersection_editor.html?u=https%3A%2F%2Fexample.com%2Fiiif%2Fimage.tif
    
  2. ページを開くと自動的に保存済みデータを読み込み
  3. 画像上の座標をクリックして記録
  4. 自動的に次の予測位置に移動
  5. 必要に応じてメモを追加
  6. CSV/JSON形式でエクスポート

複数画像プロジェクトの管理

異なる画像で複数のプロジェクトを並行して作業する場合:

  1. 各画像専用のURLブックマークを作成
  2. 各画像のデータは独立してlocalStorageに保存される
  3. 画像間でデータが混在する心配なし
  4. JSONエクスポートには画像URLも含まれる

大量データの処理

数百〜数千の座標を記録する場合:

  1. 自動遷移機能を有効化して効率化
  2. 定期的にJSON形式でバックアップ
  3. 誤りがあれば移動機能で順序を修正
  4. 完了後、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;
}

今後の改善案

  1. Undo/Redo機能: 操作履歴の実装
  2. エクスポート形式の拡張: GeoJSON、Allmaps形式への対応
  3. 協働編集: WebSocketによるリアルタイム共有
  4. 精度向上: サブピクセル精度の座標記録
  5. AIアシスト: 交点の自動検出機能

まとめ

本ツールは、URLパラメータによる柔軟な画像指定、画像ごとのデータ分離、IIIF画像の表示機能、自動遷移機能を組み合わせることで、大量の座標データを効率的に記録できる汎用的なシステムを実現しました。

参考リンク


作成日: 2025年10月
バージョン: 1.0

Discussion