diff --git a/.gitignore b/.gitignore index affff08..ce89c72 100644 --- a/.gitignore +++ b/.gitignore @@ -43,6 +43,7 @@ act-*.log tmp.txt *.tmp *.temp +tmp.md # IDE specific CLAUDE.md \ No newline at end of file diff --git a/CHANGELOG.en.md b/CHANGELOG.en.md index 4d66bfe..9ef9e5b 100644 --- a/CHANGELOG.en.md +++ b/CHANGELOG.en.md @@ -9,12 +9,65 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.1.0] - 2025-06-20 + ### Added +- **Multi-Image Navigation** + - **Navigation Panel**: New panel for easy navigation between images in a folder + - **Image List Display**: Image list and status indicators + - **ID Jump Feature**: Direct navigation to specific image by entering image ID + - **Smart Navigation**: Automatically skip non-existent images + - **View State Preservation**: Maintain zoom and pan state when switching images + - **Comparison Mode Support**: Navigate between images while comparing, with dynamic pair annotation updates + - **Keyboard Shortcuts**: Arrow keys for previous/next image navigation +- **Annotation Statistical Analysis** + - **Histogram Analysis**: 5 distribution types (width, height, area, polygon area, aspect ratio) + - **Heatmap Analysis**: 4 2D distributions (width×height, center coordinates, area×aspect ratio, etc.) + - **Detailed Statistics**: Display of mean, median, standard deviation, skewness, kurtosis, quartiles, etc. + - **Polygon Area Calculation**: Accurate segmentation area calculation using Shoelace formula + - **Data Export**: Clipboard copy functionality for statistical data + - **Menu Integration**: Shortcuts for Histogram (Ctrl/Cmd+Shift+H) and Heatmap (Ctrl/Cmd+Shift+M) +- **Common Modal Component** + - Unified modal design with blur background effects + - Responsive size support (sm, md, lg, xl) + - Accessibility support (ESC key, overlay click) +- **Comparison Mode** + - Visual comparison between two COCO datasets (ground truth vs predictions) + - Color-coded display of TP (True Positive), FP (False Positive), and FN (False Negative) + - IoU threshold-based matching algorithm + - Category mapping between different category systems + - Two IoU calculation methods: Bounding box IoU and Polygon IoU (approximation) + - Multiple matching support (1-to-many matching configuration) +- **Evaluation Metrics Display** + - Calculation and display of Precision, Recall, and F1 score + - Comparison results in statistics dialog + - Matching information in annotation detail panel +- **Sample Data Generation Enhancement** + - Pair JSON generation option for comparison testing + - Detailed matching distribution settings +- **Loading Overlay** + - Full-screen loading display for long-running operations +- **Disclaimer Addition** + - Added disclaimer in README stating statistical information is for reference purposes with developer liability limitations + ### Changed +- Improved zoom buttons to zoom relative to viewport center +- Auto-adjust view mode for single image datasets in statistics dialog +- Enhanced comparison mode data management with original data preservation +- Updated panel settings UI to place NavigationPanel as an independent tab + ### Fixed +- Fixed infinite loop issue after category mapping +- Fixed comparison results not showing for single image datasets +- Enabled comparison between datasets with different image IDs +- Fixed ReferenceError during NavigationPanel initialization +- Fixed zoom/pan state reset when switching images +- Fixed annotation data loss when changing comparison settings +- Prevented unnecessary image ID reset when switching tabs + ### Removed ## [1.0.0] - 2025-05-27 @@ -56,5 +109,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - State management with Zustand - Cross-platform support (Windows, macOS, Linux) -[Unreleased]: https://github.com/tact-software/coav/compare/v1.0.0...HEAD +[Unreleased]: https://github.com/tact-software/coav/compare/v1.1.0...HEAD +[1.1.0]: https://github.com/tact-software/coav/compare/v1.0.0...v1.1.0 [1.0.0]: https://github.com/tact-software/coav/releases/tag/v1.0.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 69e2925..d020901 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,12 +9,65 @@ ## [未リリース] +## [1.1.0] - 2025-06-20 + ### 追加 +- **マルチ画像ナビゲーション機能** + - **NavigationPanel**: フォルダ内の画像間を簡単に移動できる新しいパネル + - **画像リスト表示**: 画像一覧とステータス表示 + - **IDジャンプ機能**: 画像IDを直接入力して特定の画像へ移動 + - **スマートナビゲーション**: 存在しない画像を自動的にスキップ + - **ビュー状態保持**: 画像切り替え時もズーム・パン状態を維持 + - **比較モード対応**: 比較中でも画像間を移動でき、ペアアノテーションが動的に更新 + - **キーボードショートカット**: 矢印キーで前後の画像へ移動 +- **アノテーション統計分析機能** + - **ヒストグラム分析**: 5つの分布タイプ(幅、高さ、面積、ポリゴン面積、アスペクト比) + - **ヒートマップ分析**: 4つの2次元分布(幅×高さ、中心座標、面積×アスペクト比等) + - **詳細統計情報**: 平均、中央値、標準偏差、歪度、尖度、四分位数等の表示 + - **ポリゴン面積計算**: Shoelace公式による正確なセグメンテーション面積算出 + - **データエクスポート**: 統計データのクリップボードコピー機能 + - **メニュー統合**: ヒストグラム(Ctrl/Cmd+Shift+H)、ヒートマップ(Ctrl/Cmd+Shift+M)のショートカット +- **共通モーダルコンポーネント** + - 統一されたモーダルデザインとブラー背景効果 + - レスポンシブサイズ対応(sm, md, lg, xl) + - アクセシビリティ対応(ESCキー、オーバーレイクリック) +- **比較モード機能** + - 2つのCOCOデータセット(正解データと予測結果)の視覚的比較 + - TP(真陽性)、FP(偽陽性)、FN(偽陰性)の色分け表示 + - IoU閾値ベースのマッチングアルゴリズム + - カテゴリマッピング機能(異なるカテゴリ体系間での対応付け) + - バウンディングボックスIoUとポリゴンIoU(近似)の2つの計算方法 + - 複数マッチング対応(1対多のマッチング設定) +- **評価指標の表示** + - Precision、Recall、F1スコアの算出と表示 + - 統計ダイアログでの比較結果表示 + - アノテーション詳細パネルでのマッチング情報表示 +- **サンプルデータ生成の拡張** + - ペアJSON生成オプション(比較テスト用) + - マッチング分布の詳細設定 +- **ローディングオーバーレイ** + - 長時間処理のための全画面ローディング表示 +- **免責事項の追加** + - 統計情報が参考値である旨と開発者責任制限をREADMEに明記 + ### 変更 +- ズームボタンがビューポート中心を基準にズームするよう改善 +- 統計ダイアログで単一画像データセットのビューモードを自動調整 +- 比較モードでのデータ管理を改善し、元データを保持する仕組みを追加 +- パネル設定のUIを更新し、NavigationPanelを独立したタブに配置 + ### 修正 +- カテゴリマッピング後の無限ループ問題を修正 +- 単一画像データセットで比較結果が表示されない問題を修正 +- 異なる画像IDを持つデータセット間の比較を可能に +- NavigationPanelの初期化時のReferenceErrorを修正 +- 画像切り替え時のズーム・パン状態リセット問題を修正 +- 比較設定変更時のアノテーションデータ損失を修正 +- タブ切り替え時の不要な画像IDリセットを防止 + ### 削除 ## [1.0.0] - 2025-05-27 @@ -56,5 +109,6 @@ - Zustandによる状態管理 - クロスプラットフォーム対応(Windows、macOS、Linux) -[未リリース]: https://github.com/tact-software/coav/compare/v1.0.0...HEAD +[未リリース]: https://github.com/tact-software/coav/compare/v1.1.0...HEAD +[1.1.0]: https://github.com/tact-software/coav/compare/v1.0.0...v1.1.0 [1.0.0]: https://github.com/tact-software/coav/releases/tag/v1.0.0 diff --git a/CONTRIBUTING.en.md b/CONTRIBUTING.en.md index 52c4bc2..ad08880 100644 --- a/CONTRIBUTING.en.md +++ b/CONTRIBUTING.en.md @@ -111,11 +111,21 @@ git checkout -b feature/your-feature-name Branch naming conventions: - `feature/` - New features + - Example: `feature/auto-update`, `feature/123-screenshot-tool` - `fix/` - Bug fixes + - Example: `fix/memory-leak`, `fix/456-panel-crash` - `docs/` - Documentation + - Example: `docs/api-guide`, `docs/readme-update` - `refactor/` - Refactoring + - Example: `refactor/component-structure` - `test/` - Adding tests + - Example: `test/unit-tests`, `test/e2e-setup` - `chore/` - Other changes + - Example: `chore/dependencies-update` +- `hotfix/` - Emergency fixes (from main branch) + - Example: `hotfix/1.0.1-critical-bug` +- `release/` - Release preparation + - Example: `release/v1.1.0` ### 2. Development diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 92b4391..a027c27 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -111,11 +111,21 @@ git checkout -b feature/your-feature-name ブランチ名の規則: - `feature/` - 新機能 + - 例: `feature/auto-update`, `feature/123-screenshot-tool` - `fix/` - バグ修正 + - 例: `fix/memory-leak`, `fix/456-panel-crash` - `docs/` - ドキュメント + - 例: `docs/api-guide`, `docs/readme-update` - `refactor/` - リファクタリング + - 例: `refactor/component-structure` - `test/` - テスト追加 + - 例: `test/unit-tests`, `test/e2e-setup` - `chore/` - その他の変更 + - 例: `chore/dependencies-update` +- `hotfix/` - 緊急修正(mainブランチから) + - 例: `hotfix/1.0.1-critical-bug` +- `release/` - リリース準備 + - 例: `release/v1.1.0` ### 2. 開発 diff --git a/README.en.md b/README.en.md index bc1ae14..e1440c3 100644 --- a/README.en.md +++ b/README.en.md @@ -73,6 +73,29 @@ COAV is a COCO (Common Objects in Context) format annotation viewer designed for - Coverage and overlap rate - Per-image or dataset-wide view toggle +#### About Statistical Information + +The statistical information displayed in this application (histograms, heatmaps, numerical statistics, etc.) are **reference values** intended for data exploration and trend analysis. + +**Important Notes** + +- Polygon area calculations use approximation algorithms +- Statistical values depend on filtering settings and bin division settings +- For academic research or formal evaluation, we recommend directly checking the original annotation data + +**Intended Use** + +- Dataset overview +- Annotation distribution visualization +- Quality check assistance + +**Disclaimer** +**The developers assume no responsibility for the accuracy of statistical information or analysis results generated by this tool.** + +- We are not responsible for any loss or damage resulting from decisions or judgments based on the results of this tool +- For important analysis or research, always cross-reference with original data or other verification methods +- Commercial use or academic research use is at the user's own risk + ### 🎯 Interactive Operations - Annotation selection and highlighting diff --git a/README.md b/README.md index dfce138..595209a 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,29 @@ COAVは、機械学習の研究者、データサイエンティスト、エン - カバレッジ・重複率 - 画像単位/データセット全体の切替表示 +#### 統計情報について + +本アプリケーションで表示される統計情報(ヒストグラム、ヒートマップ、数値統計等)は、データ探索と傾向把握を目的とした**参考値**です。 + +**注意事項** + +- ポリゴン面積の計算は近似アルゴリズムを使用 +- 統計値はフィルタリング設定やビン分割設定に依存 +- 学術研究や正式な評価には、元のアノテーションデータを直接確認することを推奨 + +**用途** + +- データセットの概要把握 +- アノテーション分布の可視化 +- 品質チェックの補助 + +**免責事項** +**本ツールで生成される統計情報や分析結果の正確性について、開発者は一切の責任を負いません。** + +- 本ツールの結果に基づく意思決定や判断による損失・損害について責任を負いかねます +- 重要な分析や研究においては、必ず元データや他の検証手段との照合を行ってください +- 商用利用や学術研究での使用は、ユーザー自身の責任において行ってください + ### 🎯 インタラクティブな操作 - アノテーションの選択・ハイライト diff --git a/bun.lock b/bun.lock index 432bd61..f28e873 100644 --- a/bun.lock +++ b/bun.lock @@ -4,16 +4,20 @@ "": { "name": "coav", "dependencies": { + "@nivo/core": "^0.99.0", + "@nivo/heatmap": "^0.99.0", "@tauri-apps/api": "^2", "@tauri-apps/plugin-dialog": "^2.2.2", "@tauri-apps/plugin-fs": "^2.3.0", "@tauri-apps/plugin-opener": "^2", "i18next": "^25.2.1", "konva": "^9.3.20", + "lucide-react": "^0.511.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-i18next": "^15.5.2", "react-konva": "18", + "recharts": "^2.15.3", "zustand": "^5.0.5", }, "devDependencies": { @@ -188,6 +192,26 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], + "@nivo/annotations": ["@nivo/annotations@0.99.0", "", { "dependencies": { "@nivo/colors": "0.99.0", "@nivo/core": "0.99.0", "@nivo/theming": "0.99.0", "@react-spring/web": "9.4.5 || ^9.7.2 || ^10.0", "lodash": "^4.17.21" }, "peerDependencies": { "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" } }, "sha512-jCuuXPbvpaqaz4xF7k5dv0OT2ubn5Nt0gWryuTe/8oVsC/9bzSuK8bM9vBty60m9tfO+X8vUYliuaCDwGksC2g=="], + + "@nivo/axes": ["@nivo/axes@0.99.0", "", { "dependencies": { "@nivo/core": "0.99.0", "@nivo/scales": "0.99.0", "@nivo/text": "0.99.0", "@nivo/theming": "0.99.0", "@react-spring/web": "9.4.5 || ^9.7.2 || ^10.0", "@types/d3-format": "^1.4.1", "@types/d3-time-format": "^2.3.1", "d3-format": "^1.4.4", "d3-time-format": "^3.0.0" }, "peerDependencies": { "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" } }, "sha512-3KschnmEL0acRoa7INSSOSEFwJLm54aZwSev7/r8XxXlkgRBriu6ReZy/FG0wfN+ljZ4GMvx+XyIIf6kxzvrZg=="], + + "@nivo/colors": ["@nivo/colors@0.99.0", "", { "dependencies": { "@nivo/core": "0.99.0", "@nivo/theming": "0.99.0", "@types/d3-color": "^3.0.0", "@types/d3-scale": "^4.0.8", "@types/d3-scale-chromatic": "^3.0.0", "d3-color": "^3.1.0", "d3-scale": "^4.0.2", "d3-scale-chromatic": "^3.0.0", "lodash": "^4.17.21" }, "peerDependencies": { "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" } }, "sha512-hyYt4lEFIfXOUmQ6k3HXm3KwhcgoJpocmoGzLUqzk7DzuhQYJo+4d5jIGGU0N/a70+9XbHIdpKNSblHAIASD3w=="], + + "@nivo/core": ["@nivo/core@0.99.0", "", { "dependencies": { "@nivo/theming": "0.99.0", "@nivo/tooltip": "0.99.0", "@react-spring/web": "9.4.5 || ^9.7.2 || ^10.0", "@types/d3-shape": "^3.1.6", "d3-color": "^3.1.0", "d3-format": "^1.4.4", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-scale-chromatic": "^3.0.0", "d3-shape": "^3.2.0", "d3-time-format": "^3.0.0", "lodash": "^4.17.21", "react-virtualized-auto-sizer": "^1.0.26", "use-debounce": "^10.0.4" }, "peerDependencies": { "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" } }, "sha512-olCItqhPG3xHL5ei+vg52aB6o+6S+xR2idpkd9RormTTUniZb8U2rOdcQojOojPY5i9kVeQyLFBpV4YfM7OZ9g=="], + + "@nivo/heatmap": ["@nivo/heatmap@0.99.0", "", { "dependencies": { "@nivo/annotations": "0.99.0", "@nivo/axes": "0.99.0", "@nivo/colors": "0.99.0", "@nivo/core": "0.99.0", "@nivo/legends": "0.99.0", "@nivo/scales": "0.99.0", "@nivo/text": "0.99.0", "@nivo/theming": "0.99.0", "@nivo/tooltip": "0.99.0", "@react-spring/core": "9.4.5 || ^9.7.2 || ^10.0", "@react-spring/web": "9.4.5 || ^9.7.2 || ^10.0", "@types/d3-scale": "^4.0.8", "d3-scale": "^4.0.2" }, "peerDependencies": { "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" } }, "sha512-cnd5V6XYLvHtQ8GObUyYyEgmAUa5/ERYVmNXR1znA+yzmB+tGthJsOeGar+ntCb43pIL5HbhEaD3YpsYzBxOhQ=="], + + "@nivo/legends": ["@nivo/legends@0.99.0", "", { "dependencies": { "@nivo/colors": "0.99.0", "@nivo/core": "0.99.0", "@nivo/text": "0.99.0", "@nivo/theming": "0.99.0", "@types/d3-scale": "^4.0.8", "d3-scale": "^4.0.2" }, "peerDependencies": { "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" } }, "sha512-P16FjFqNceuTTZphINAh5p0RF0opu3cCKoWppe2aRD9IuVkvRm/wS5K1YwMCxDzKyKh5v0AuTlu9K6o3/hk8hA=="], + + "@nivo/scales": ["@nivo/scales@0.99.0", "", { "dependencies": { "@types/d3-interpolate": "^3.0.4", "@types/d3-scale": "^4.0.8", "@types/d3-time": "^1.1.1", "@types/d3-time-format": "^3.0.0", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-time": "^1.0.11", "d3-time-format": "^3.0.0", "lodash": "^4.17.21" } }, "sha512-g/2K4L6L8si6E2BWAHtFVGahtDKbUcO6xHJtlIZMwdzaJc7yB16EpWLK8AfI/A42KadLhJSJqBK3mty+c7YZ+w=="], + + "@nivo/text": ["@nivo/text@0.99.0", "", { "dependencies": { "@nivo/core": "0.99.0", "@nivo/theming": "0.99.0", "@react-spring/web": "9.4.5 || ^9.7.2 || ^10.0" }, "peerDependencies": { "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" } }, "sha512-ho3oZpAZApsJNjsIL5WJSAdg/wjzTBcwo1KiHBlRGUmD+yUWO8qp7V+mnYRhJchwygtRVALlPgZ/rlcW2Xr/MQ=="], + + "@nivo/theming": ["@nivo/theming@0.99.0", "", { "dependencies": { "lodash": "^4.17.21" }, "peerDependencies": { "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" } }, "sha512-KvXlf0nqBzh/g2hAIV9bzscYvpq1uuO3TnFN3RDXGI72CrbbZFTGzprPju3sy/myVsauv+Bb+V4f5TZ0jkYKRg=="], + + "@nivo/tooltip": ["@nivo/tooltip@0.99.0", "", { "dependencies": { "@nivo/core": "0.99.0", "@nivo/theming": "0.99.0", "@react-spring/web": "9.4.5 || ^9.7.2 || ^10.0" }, "peerDependencies": { "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" } }, "sha512-weoEGR3xAetV4k2P6k96cdamGzKQ5F2Pq+uyDaHr1P3HYArM879Pl+x+TkU0aWjP6wgUZPx/GOBiV1Hb1JxIqg=="], + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], @@ -200,6 +224,18 @@ "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], + "@react-spring/animated": ["@react-spring/animated@9.4.5", "", { "dependencies": { "@react-spring/shared": "~9.4.5", "@react-spring/types": "~9.4.5" }, "peerDependencies": { "react": "^16.8.0 || >=17.0.0 || >=18.0.0" } }, "sha512-KWqrtvJSMx6Fj9nMJkhTwM9r6LIriExDRV6YHZV9HKQsaolUFppgkOXpC+rsL1JEtEvKv6EkLLmSqHTnuYjiIA=="], + + "@react-spring/core": ["@react-spring/core@9.4.5", "", { "dependencies": { "@react-spring/animated": "~9.4.5", "@react-spring/rafz": "~9.4.5", "@react-spring/shared": "~9.4.5", "@react-spring/types": "~9.4.5" }, "peerDependencies": { "react": "^16.8.0 || >=17.0.0 || >=18.0.0" } }, "sha512-83u3FzfQmGMJFwZLAJSwF24/ZJctwUkWtyPD7KYtNagrFeQKUH1I05ZuhmCmqW+2w1KDW1SFWQ43RawqfXKiiQ=="], + + "@react-spring/rafz": ["@react-spring/rafz@9.4.5", "", {}, "sha512-swGsutMwvnoyTRxvqhfJBtGM8Ipx6ks0RkIpNX9F/U7XmyPvBMGd3GgX/mqxZUpdlsuI1zr/jiYw+GXZxAlLcQ=="], + + "@react-spring/shared": ["@react-spring/shared@9.4.5", "", { "dependencies": { "@react-spring/rafz": "~9.4.5", "@react-spring/types": "~9.4.5" }, "peerDependencies": { "react": "^16.8.0 || >=17.0.0 || >=18.0.0" } }, "sha512-JhMh3nFKsqyag0KM5IIM8BQANGscTdd0mMv3BXsUiMZrcjQTskyfnv5qxEeGWbJGGar52qr5kHuBHtCjQOzniA=="], + + "@react-spring/types": ["@react-spring/types@9.4.5", "", {}, "sha512-mpRIamoHwql0ogxEUh9yr4TP0xU5CWyZxVQeccGkHHF8kPMErtDXJlxyo0lj+telRF35XNihtPTWoflqtyARmg=="], + + "@react-spring/web": ["@react-spring/web@9.4.5", "", { "dependencies": { "@react-spring/animated": "~9.4.5", "@react-spring/core": "~9.4.5", "@react-spring/shared": "~9.4.5", "@react-spring/types": "~9.4.5" }, "peerDependencies": { "react": "^16.8.0 || >=17.0.0 || >=18.0.0", "react-dom": "^16.8.0 || >=17.0.0 || >=18.0.0" } }, "sha512-NGAkOtKmOzDEctL7MzRlQGv24sRce++0xAY7KlcxmeVkR7LRSGkoXHaIfm9ObzxPMcPHQYQhf3+X9jepIFNHQA=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.9", "", {}, "sha512-e9MeMtVWo186sgvFFJOPGy7/d2j2mZhLJIdVW0C/xDluuOvymEATqz6zKsP0ZmXGzQtqlyjz5sC1sYQUoJG98w=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.41.1", "", { "os": "android", "cpu": "arm" }, "sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw=="], @@ -290,6 +326,30 @@ "@types/babel__traverse": ["@types/babel__traverse@7.20.7", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng=="], + "@types/d3-array": ["@types/d3-array@3.2.1", "", {}, "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg=="], + + "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], + + "@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="], + + "@types/d3-format": ["@types/d3-format@1.4.5", "", {}, "sha512-mLxrC1MSWupOSncXN/HOlWUAAIffAEBaI4+PKy2uMPsKe4FNZlk7qrbTjmzJXITQQqBHivaks4Td18azgqnotA=="], + + "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], + + "@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="], + + "@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="], + + "@types/d3-scale-chromatic": ["@types/d3-scale-chromatic@3.1.0", "", {}, "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ=="], + + "@types/d3-shape": ["@types/d3-shape@3.1.7", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg=="], + + "@types/d3-time": ["@types/d3-time@1.1.4", "", {}, "sha512-JIvy2HjRInE+TXOmIGN5LCmeO0hkFZx5f9FZ7kiN+D+YTcc8pptsiLiuHsvwxwC7VVKmJ2ExHUgNlAiV7vQM9g=="], + + "@types/d3-time-format": ["@types/d3-time-format@2.3.4", "", {}, "sha512-xdDXbpVO74EvadI3UDxjxTdR6QIxm1FKzEA/+F8tL4GWWUg/hgvBqf6chql64U5A9ZUGWo7pEu4eNlyLwbKdhg=="], + + "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="], + "@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="], "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], @@ -402,6 +462,8 @@ "check-error": ["check-error@2.1.1", "", {}, "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw=="], + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], @@ -418,6 +480,30 @@ "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], + + "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], + + "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="], + + "d3-format": ["d3-format@1.4.5", "", {}, "sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ=="], + + "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], + + "d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="], + + "d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="], + + "d3-scale-chromatic": ["d3-scale-chromatic@3.1.0", "", { "dependencies": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" } }, "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ=="], + + "d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="], + + "d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="], + + "d3-time-format": ["d3-time-format@3.0.0", "", { "dependencies": { "d3-time": "1 - 2" } }, "sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag=="], + + "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], + "data-urls": ["data-urls@5.0.0", "", { "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0" } }, "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg=="], "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], @@ -430,6 +516,8 @@ "decimal.js": ["decimal.js@10.5.0", "", {}, "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw=="], + "decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="], + "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], @@ -444,6 +532,8 @@ "dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="], + "dom-helpers": ["dom-helpers@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], @@ -502,10 +592,14 @@ "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + "eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], + "expect-type": ["expect-type@1.2.1", "", {}, "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + "fast-equals": ["fast-equals@5.2.2", "", {}, "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw=="], + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], @@ -598,6 +692,8 @@ "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], + "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], + "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], "is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="], @@ -704,6 +800,8 @@ "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + "lucide-react": ["lucide-react@0.511.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-VK5a2ydJ7xm8GvBeKLS9mu1pVK6ucef9780JVUjw6bAjJL/QXnd4Y0p7SPeOUMC27YhzNCZvm5d/QX0Tp3rc0w=="], + "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], @@ -806,7 +904,7 @@ "react-i18next": ["react-i18next@15.5.2", "", { "dependencies": { "@babel/runtime": "^7.25.0", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 23.2.3", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-ePODyXgmZQAOYTbZXQn5rRsSBu3Gszo69jxW6aKmlSgxKAI1fOhDwSu6bT4EKHciWPKQ7v7lPrjeiadR6Gi+1A=="], - "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], "react-konva": ["react-konva@18.2.10", "", { "dependencies": { "@types/react-reconciler": "^0.28.2", "its-fine": "^1.1.1", "react-reconciler": "~0.29.0", "scheduler": "^0.23.0" }, "peerDependencies": { "konva": "^8.0.1 || ^7.2.5 || ^9.0.0", "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-ohcX1BJINL43m4ynjZ24MxFI1syjBdrXhqVxYVDw2rKgr3yuS0x/6m1Y2Z4sl4T/gKhfreBx8KHisd0XC6OT1g=="], @@ -814,6 +912,16 @@ "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], + "react-smooth": ["react-smooth@4.0.4", "", { "dependencies": { "fast-equals": "^5.0.1", "prop-types": "^15.8.1", "react-transition-group": "^4.4.5" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q=="], + + "react-transition-group": ["react-transition-group@4.4.5", "", { "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", "loose-envify": "^1.4.0", "prop-types": "^15.6.2" }, "peerDependencies": { "react": ">=16.6.0", "react-dom": ">=16.6.0" } }, "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g=="], + + "react-virtualized-auto-sizer": ["react-virtualized-auto-sizer@1.0.26", "", { "peerDependencies": { "react": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-CblNyiNVw2o+hsa5/49NH2ogGxZ+t+3aweRvNSq7TVjDIlwk7ir4lencEg5HxHeSzwNarSkNkiu0qJSOXtxm5A=="], + + "recharts": ["recharts@2.15.3", "", { "dependencies": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", "lodash": "^4.17.21", "react-is": "^18.3.1", "react-smooth": "^4.0.4", "recharts-scale": "^0.4.4", "tiny-invariant": "^1.3.1", "victory-vendor": "^36.6.8" }, "peerDependencies": { "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-EdOPzTwcFSuqtvkDoaM5ws/Km1+WTAO2eizL7rqiG0V2UVhTnz0m7J2i0CjVPUCdEkZImaWvXLbZDS2H5t6GFQ=="], + + "recharts-scale": ["recharts-scale@0.4.5", "", { "dependencies": { "decimal.js-light": "^2.4.1" } }, "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w=="], + "redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="], "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], @@ -906,6 +1014,8 @@ "test-exclude": ["test-exclude@7.0.1", "", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^10.4.1", "minimatch": "^9.0.4" } }, "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg=="], + "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], @@ -952,6 +1062,10 @@ "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "use-debounce": ["use-debounce@10.0.4", "", { "peerDependencies": { "react": "*" } }, "sha512-6Cf7Yr7Wk7Kdv77nnJMf6de4HuDE4dTxKij+RqE9rufDsI6zsbjyAxcH5y2ueJCQAnfgKbzXbZHYlkFwmBlWkw=="], + + "victory-vendor": ["victory-vendor@36.9.2", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ=="], + "vite": ["vite@6.3.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ=="], "vite-node": ["vite-node@3.1.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.0", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-6enNwYnpyDo4hEgytbmc6mYWHXDHYEn0D1/rw4Q+tnHUGtKTJsn8T1YkX6Q18wI5LCrS8CTYlBaiCqxOy2kvUA=="], @@ -1010,16 +1124,28 @@ "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], + "@nivo/scales/@types/d3-time-format": ["@types/d3-time-format@3.0.4", "", {}, "sha512-or9DiDnYI1h38J9hxKEsw513+KVuFbEVhl7qdxcaudoiqWWepapUen+2vAriFGexr6W5+P4l9+HJrB39GG+oRg=="], + + "@nivo/scales/d3-time": ["d3-time@1.1.0", "", {}, "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA=="], + "@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], "@testing-library/dom/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], + "@types/d3-scale/@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="], + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "@typescript-eslint/typescript-estree/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + "d3-scale/d3-format": ["d3-format@3.1.0", "", {}, "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA=="], + + "d3-scale/d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="], + + "d3-time-format/d3-time": ["d3-time@2.1.1", "", { "dependencies": { "d3-array": "2" } }, "sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ=="], + "eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], @@ -1040,6 +1166,8 @@ "pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], + "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -1048,6 +1176,8 @@ "test-exclude/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "victory-vendor/@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="], + "wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], "wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -1056,10 +1186,14 @@ "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + "d3-time-format/d3-time/d3-array": ["d3-array@2.12.1", "", { "dependencies": { "internmap": "^1.0.0" } }, "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ=="], + "glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], "test-exclude/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "d3-time-format/d3-time/d3-array/internmap": ["internmap@1.0.1", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="], } } diff --git a/docs/BRANCHING_STRATEGY.md b/docs/BRANCHING_STRATEGY.md new file mode 100644 index 0000000..ab53f35 --- /dev/null +++ b/docs/BRANCHING_STRATEGY.md @@ -0,0 +1,464 @@ +# ブランチ運用戦略 + +## 概要 + +COAVプロジェクトでは、効率的な開発とリリース管理のため、以下のブランチ戦略を採用します。 + +## ブランチフロー図 + +```mermaid +gitGraph + commit id: "v1.0.0" + branch develop + checkout develop + commit id: "開発開始" + + branch feature/test-infrastructure + checkout feature/test-infrastructure + commit id: "test: Vitest setup" + commit id: "test: unit tests" + checkout develop + merge feature/test-infrastructure + + branch feature/ci-cd + checkout feature/ci-cd + commit id: "ci: GitHub Actions" + commit id: "ci: test workflow" + checkout develop + merge feature/ci-cd + + branch feature/screenshots + checkout feature/screenshots + commit id: "docs: add screenshots" + checkout develop + merge feature/screenshots + + checkout develop + branch release/v1.1.0 + checkout release/v1.1.0 + commit id: "chore: v1.1.0" + checkout main + merge release/v1.1.0 tag: "v1.1.0" + checkout develop + merge release/v1.1.0 +``` + +## 開発フロー詳細 + +```mermaid +flowchart TD + A[main - 安定版] -->|新規開発開始| B[develop - 統合] + B -->|機能開発| C[feature/機能名] + B -->|バグ修正| D[fix/バグ名] + C -->|PR&レビュー| B + D -->|PR&レビュー| B + B -->|リリース準備| E[release/vX.Y.Z] + E -->|リリース| A + E -->|マージバック| B + A -->|緊急修正| F[hotfix/修正名] + F -->|修正完了| A + F -->|マージバック| B + + style A fill:#90EE90 + style B fill:#87CEEB + style C fill:#FFB6C1 + style D fill:#FFB6C1 + style E fill:#DDA0DD + style F fill:#FFA500 +``` + +## ブランチ構成 + +### 🌟 メインブランチ + +#### `main` +- **用途**: プロダクション準備完了のコード +- **特徴**: 常に安定していて、リリース可能な状態 +- **保護**: 直接pushは禁止、PRのみでマージ + +#### `develop` +- **用途**: 次回リリースの開発統合ブランチ +- **特徴**: 新機能が統合される場所 +- **マージ元**: featureブランチ、fixブランチ + +### 📝 作業ブランチ + +#### `feature/*` +- **用途**: 新機能開発 +- **命名規則**: `feature/機能名` または `feature/issue番号-機能名` +- **例**: + - `feature/auto-update` + - `feature/123-screenshot-tool` + - `feature/test-infrastructure` +- **ベース**: `develop` +- **マージ先**: `develop` + +#### `fix/*` +- **用途**: バグ修正(緊急でないもの) +- **命名規則**: `fix/バグ内容` または `fix/issue番号-バグ内容` +- **例**: + - `fix/memory-leak` + - `fix/456-panel-crash` +- **ベース**: `develop` +- **マージ先**: `develop` + +#### `docs/*` +- **用途**: ドキュメントの更新 +- **命名規則**: `docs/内容` +- **例**: + - `docs/api-guide` + - `docs/readme-update` +- **ベース**: `develop` +- **マージ先**: `develop` + +#### `refactor/*` +- **用途**: コードのリファクタリング +- **命名規則**: `refactor/対象` +- **例**: `refactor/component-structure` +- **ベース**: `develop` +- **マージ先**: `develop` + +#### `test/*` +- **用途**: テストの追加・修正 +- **命名規則**: `test/内容` +- **例**: + - `test/unit-tests` + - `test/e2e-setup` +- **ベース**: `develop` +- **マージ先**: `develop` + +#### `chore/*` +- **用途**: ビルド設定、依存関係更新など +- **命名規則**: `chore/内容` +- **例**: `chore/dependencies-update` +- **ベース**: `develop` +- **マージ先**: `develop` + +#### `hotfix/*` +- **用途**: 緊急バグ修正 +- **命名規則**: `hotfix/バージョン-内容` +- **例**: `hotfix/1.0.1-critical-bug` +- **ベース**: `main` +- **マージ先**: `main` と `develop` + +#### `release/*` +- **用途**: リリース準備 +- **命名規則**: `release/vX.Y.Z` +- **例**: `release/v1.1.0` +- **ベース**: `develop` +- **マージ先**: `main` と `develop` + +## ワークフロー例 + +### 1. 新機能開発(テスト環境構築の例) + +```bash +# developブランチから開始 +git checkout develop +git pull origin develop + +# featureブランチ作成 +git checkout -b feature/test-infrastructure + +# 作業とコミット +# ... ユニットテスト環境追加 +git add . +git commit -m "test: add unit test infrastructure with Vitest" + +# ... E2Eテスト環境追加 +git add . +git commit -m "test: add E2E test setup with Playwright" + +# ... CI設定追加 +git add . +git commit -m "ci: add GitHub Actions workflow for testing" + +# developにマージ(PR経由) +git push origin feature/test-infrastructure +# GitHub上でPRを作成 → レビュー → マージ +``` + +### 2. 複数機能の並行開発 + +```bash +# 開発者A: スクリーンショット機能 +git checkout -b feature/screenshot-tool develop + +# 開発者B: 自動アップデート機能 +git checkout -b feature/auto-update develop + +# それぞれ独立して開発・コミット +# 完成したものから順次developへマージ +``` + +### 3. リリース準備(v1.1.0の例) + +```bash +# developからリリースブランチ作成 +git checkout -b release/v1.1.0 develop + +# バージョン更新 +bun version:update 1.1.0 + +# CHANGELOG更新 +# ... CHANGELOG.mdを編集 + +git add . +git commit -m "chore: prepare release v1.1.0" + +# 最終テスト・修正 +# ... 必要に応じて修正 + +# mainとdevelopへマージ +git checkout main +git merge --no-ff release/v1.1.0 +git tag -a v1.1.0 -m "Release version 1.1.0" + +git checkout develop +git merge --no-ff release/v1.1.0 +``` + +## 推奨される運用ルール + +### developブランチへの直接コミット + +原則として **developブランチへの直接コミットは避ける** ことを推奨します。 + +#### ❌ 避けるべきケース +- 新機能の追加 +- 大きなリファクタリング +- 複雑なバグ修正 +- 破壊的変更を含む修正 + +#### ⭕ 許容されるケース +- **軽微な修正** + - タイポの修正 + - コメントの追加・修正 + - 明らかなバグの1行修正 +- **緊急対応** + - ビルドが壊れた時の修正 + - CI/CDの緊急修正 +- **メンテナンス作業** + - バージョン番号の更新 + - 依存関係の軽微なアップデート + +#### 推奨フロー +```bash +# ❌ 避けるべき +git checkout develop +git commit -m "feat: 新機能追加" # 直接コミット + +# ⭕ 推奨 +git checkout -b feature/new-feature develop +git commit -m "feat: 新機能追加" +git push origin feature/new-feature +# PRを作成してレビュー後マージ +``` + +### 小さな機能の扱い方 + +機能の大きさに応じて、適切な方法を選択します。 + +#### 🏃 短期ブランチ(推奨) +**作業時間: 数時間〜1日程度** + +小さな機能でも原則ブランチを作成しますが、素早くマージします: + +```bash +# 朝: ブランチ作成 +git checkout -b feature/add-tooltip develop + +# 作業・コミット(1-3コミット程度) +git add . +git commit -m "feat: add tooltip to annotation panel" + +# 夕方: PR作成・レビュー・マージ +git push origin feature/add-tooltip +# 即座にPR → レビュー → マージ(同日中) +``` + +**メリット**: +- レビューの機会を確保 +- 履歴が明確 +- CIでのテスト実行 + +#### 🎯 関連機能のグループ化 +**複数の小機能がある場合** + +関連する小機能は1つのブランチにまとめることも可能: + +```bash +git checkout -b feature/ui-improvements develop + +# 複数の小さな改善 +git commit -m "feat: add tooltip to annotation panel" +git commit -m "feat: improve button hover states" +git commit -m "feat: add keyboard shortcut hints" + +# まとめてPR +``` + +#### 📏 判断基準 + +| 項目 | 個別ブランチ | グループ化 | develop直接 | +|------|------------|-----------|------------| +| 機能の独立性 | 高い | 関連あり | - | +| 作業時間 | 1日以上 | 数時間〜1日 | 30分以内 | +| コミット数 | 3以上 | 2-5 | 1 | +| 影響範囲 | 中〜大 | 小〜中 | 極小 | +| レビュー必要性 | 必須 | 推奨 | 任意 | + +#### 実例 + +```bash +# ⭕ ブランチ作成すべき例(小さくても) +- 新しいアイコンの追加とその使用 +- ショートカットキーの追加 +- 新しいユーティリティ関数の追加 + +# 🤔 グループ化できる例 +- 複数のUIツールチップ追加 +- 関連する複数のアイコン追加 +- 同一画面の複数の小改善 + +# ⭕ develop直接でも可の例 +- READMEのタイポ修正(1文字) +- コメントの文言修正 +- 既存の定数の値を1つ変更 +``` + +### コミット単位 +- **機能単位**: 1つの機能追加は1つのコミット +- **論理的な単位**: 関連する変更はまとめる +- **レビュー可能**: 大きすぎず小さすぎないサイズ + +### PR作成タイミング +- **早期PR**: WIP (Work In Progress) でも早めにPRを作成 +- **ドラフトPR**: 開発中はドラフトとしてマーク +- **レビュー準備**: 完成したらReady for reviewに変更 + +### マージ戦略 +- **Squash and merge**: featureブランチは通常これを使用 +- **Create a merge commit**: releaseブランチはこれを使用 +- **Rebase and merge**: 単純な修正のみ + +## 次期リリース(v1.1.0)に向けた計画例 + +```mermaid +graph LR + A[develop] --> B[feature/test-infrastructure
テスト環境構築] + A --> C[feature/ci-cd-pipeline
CI/CD設定] + A --> D[feature/screenshot-demo
スクリーンショット追加] + A --> E[feature/binary-release
バイナリリリース自動化] + A --> F[fix/markdown-lint
Markdownlint警告修正] + + B --> G[develop
統合] + C --> G + D --> G + E --> G + F --> G + + G --> H[release/v1.1.0] + H --> I[main
v1.1.0] + + style A fill:#87CEEB + style G fill:#87CEEB + style H fill:#DDA0DD + style I fill:#90EE90 +``` + +各機能は独立して開発し、完成次第developにマージしていきます。 + +## タイムライン例 + +```mermaid +gantt + title v1.1.0 開発スケジュール + dateFormat YYYY-MM-DD + section 環境整備 + テスト環境構築 :a1, 2024-01-15, 7d + CI/CD設定 :a2, after a1, 5d + section 機能追加 + スクリーンショット :b1, 2024-01-15, 3d + バイナリリリース自動化 :b2, after a2, 7d + section 品質改善 + Markdownlint修正 :c1, 2024-01-15, 2d + section リリース + リリース準備 :d1, after b2, 3d + v1.1.0リリース :milestone, after d1 +``` + +## 利点 + +1. **並行開発**: 複数の開発者が独立して作業可能 +2. **履歴の明確化**: 機能単位でのコミット履歴 +3. **安定性**: mainブランチは常に安定 +4. **柔軟性**: 機能の完成順序に依存しない + +## ブランチの削除ポリシー + +### 🗑️ 削除するブランチ + +マージ後は以下のブランチを削除します: + +- **`feature/*`** - マージ後即削除 +- **`fix/*`** - マージ後即削除 +- **`docs/*`** - マージ後即削除 +- **`refactor/*`** - マージ後即削除 +- **`test/*`** - マージ後即削除 +- **`chore/*`** - マージ後即削除 +- **`hotfix/*`** - マージ後即削除 +- **`release/*`** - マージ後即削除 + +### 🔒 保持するブランチ + +以下のブランチは削除しません: + +- **`main`** - 永続的 +- **`develop`** - 永続的 + +### 削除の実行方法 + +#### GitHub上での自動削除(推奨) +PRマージ時に「Delete branch」ボタンで削除、または自動削除を設定: + +``` +リポジトリ設定 → General → Pull Requests +☑️ Automatically delete head branches +``` + +#### ローカルでの削除 + +```bash +# リモートブランチの削除(マージ済み) +git push origin --delete feature/your-feature + +# ローカルブランチの削除 +git branch -d feature/your-feature + +# マージされていないブランチの強制削除(注意) +git branch -D feature/your-feature + +# 削除済みリモートブランチの追跡を削除 +git fetch --prune +``` + +#### 一括クリーンアップ + +```bash +# マージ済みのローカルブランチを一覧表示 +git branch --merged develop | grep -v -E "(main|develop)" + +# マージ済みのローカルブランチを一括削除 +git branch --merged develop | grep -v -E "(main|develop)" | xargs -n 1 git branch -d + +# リモートの削除済みブランチをローカルから削除 +git remote prune origin +``` + +## 注意点 + +- `develop`ブランチは定期的に`main`の変更を取り込む +- 長期間のfeatureブランチは定期的に`develop`をマージ +- コンフリクトは早期に解決 +- マージ完了したブランチは速やかに削除してリポジトリを整理 \ No newline at end of file diff --git a/docs/COMPARISON_MODE_EVALUATION.md b/docs/COMPARISON_MODE_EVALUATION.md new file mode 100644 index 0000000..76d52a3 --- /dev/null +++ b/docs/COMPARISON_MODE_EVALUATION.md @@ -0,0 +1,164 @@ +# 比較モード評価方法の詳細仕様 + +## 概要 + +COAVの比較モードは、2つのCOCOフォーマットのアノテーションデータセットを比較し、物体検出の性能評価を行います。本ドキュメントでは、評価に使用される各種アルゴリズムと計算方法について詳細に説明します。 + +## IoU(Intersection over Union)の計算方法 + +### 1. バウンディングボックスIoU + +バウンディングボックス形式のアノテーションに対しては、標準的なIoU計算を実装しています: + +``` +IoU = 交差面積 / 結合面積 +``` + +具体的な計算手順: +1. 2つのバウンディングボックスの交差領域を計算 + - `intersectionX1 = max(box1.x1, box2.x1)` + - `intersectionY1 = max(box1.y1, box2.y1)` + - `intersectionX2 = min(box1.x2, box2.x2)` + - `intersectionY2 = min(box1.y2, box2.y2)` +2. 交差面積を計算 + - 交差が存在しない場合(`x2 < x1` または `y2 < y1`)は0を返す + - 存在する場合:`交差面積 = (x2 - x1) * (y2 - y1)` +3. 結合面積を計算 + - `結合面積 = box1の面積 + box2の面積 - 交差面積` +4. IoU = 交差面積 / 結合面積 + +### 2. ポリゴンIoU + +セグメンテーション(ポリゴン)形式のアノテーションに対しては、グリッドベースの近似アルゴリズムを使用します: + +1. **グリッド生成** + - 両ポリゴンを包含するバウンディングボックスを計算 + - グリッドサイズを動的に決定:`gridSize = max(1, min(幅/50, 高さ/50))` + - これにより、領域を約50×50のグリッドに分割(精度と性能のバランス) + +2. **サンプリングポイント** + - **ポイントはピクセルではなく、画像座標系上のサンプリング点** + - グリッド間隔(gridSize)で等間隔にサンプリング + - 例:200×200ピクセルの領域の場合、gridSize=4となり、4ピクセル間隔でサンプリング + - 総サンプリング数は領域のサイズに依存(通常2,500点程度) + +3. **ポイント内外判定(Ray Casting Algorithm)** + - 各サンプリングポイントがポリゴン内にあるかを判定 + - 判定方法:ポイントから右方向に半直線を引き、ポリゴンの辺との交差回数をカウント + - 交差回数が奇数:ポイントは内部 + - 交差回数が偶数:ポイントは外部 + +4. **IoU計算** + - 両ポリゴンに含まれるサンプリングポイント数:交差ポイント数 + - いずれかのポリゴンに含まれるサンプリングポイント数:結合ポイント数 + - `IoU = 交差ポイント数 / 結合ポイント数` + +**注意事項**: +- この手法は近似アルゴリズムであり、真のポリゴン面積ではない +- グリッドが細かいほど精度は向上するが、計算時間も増加 +- 実装では性能を考慮し、適応的なグリッドサイズを使用 + +## TP/FP/FNの分類基準 + +### 基本的な分類ルール + +1. **True Positive (TP)** + - Ground Truth(GT)とPredictionの間でマッチングが成立したアノテーション + - マッチング条件:`IoU ≥ 閾値`(デフォルト:0.5) + - 同じカテゴリIDまたはカテゴリマッピングで対応付けられたカテゴリ + +2. **False Positive (FP)** + - Predictionデータセット内で、どのGTアノテーションともマッチしなかったアノテーション + +3. **False Negative (FN)** + - GTデータセット内で、どのPredictionともマッチしなかったアノテーション + +### GT/Predictionの指定 + +- 設定で`gtFileId`を指定することで、どちらのファイルをGTとして扱うかを制御 +- 未指定の場合は最初に読み込んだファイルがGT + +## 最大マッチング数が2以上の場合の挙動 + +### アルゴリズムの詳細 + +`maxMatchesPerAnnotation > 1`の場合、1つのアノテーションが複数のアノテーションとマッチすることを許可します。これは、密集した物体や重なりのある場合に有用です。 + +**マッチングアルゴリズム(Greedy Assignment)**: + +1. **全ペアのIoU計算** + - GTとPredictionの全組み合わせに対してIoUを計算 + - カテゴリマッピングに従い、対応可能なカテゴリ間のみ計算 + +2. **IoUによるソート** + - 計算された全マッチをIoUの降順でソート + - 最も高いIoUを持つマッチから優先的に処理 + +3. **貪欲法による割り当て** + ``` + for each match in sorted_matches: + if (gt_match_count < maxMatches) AND (pred_match_count < maxMatches): + assign match + increment match counts + ``` + +4. **結果の特性** + - 各アノテーションは最大で`maxMatchesPerAnnotation`個のマッチを持つ + - IoUが高いマッチが優先される + - 一度マッチ数上限に達したアノテーションは、それ以上マッチしない + +### 例:maxMatches=2の場合 + +``` +GT: [A, B] +Pred: [1, 2, 3] +IoU値: A-1:0.8, A-2:0.7, A-3:0.6, B-1:0.75, B-2:0.65 + +処理順序(IoU降順): +1. A-1 (0.8) → マッチ成立 +2. B-1 (0.75) → マッチ成立 +3. A-2 (0.7) → マッチ成立(Aの2個目) +4. B-2 (0.65) → マッチ成立(Bの2個目) +5. A-3 (0.6) → 却下(Aが既に上限) +``` + +## カテゴリマッピング機能 + +異なるカテゴリ体系を持つデータセット間の比較を可能にします: + +- 1対多マッピングをサポート +- 例:`車両(id:1)` → `自動車(id:2), トラック(id:3), バス(id:4)` +- マッピングはUIから設定可能 + +## 統計情報の計算 + +### カテゴリ別統計 +- **Precision** = TP / (TP + FP) +- **Recall** = TP / (TP + FN) +- **F1 Score** = 2 × (Precision × Recall) / (Precision + Recall) + +### 全体統計 +- 全カテゴリのTP、FP、FNを合計して計算 + +## 品質評価のための追加機能 + +### 1. 閾値未満マッチの追跡 +- `0 < IoU < 閾値`のマッチも記録 +- 閾値調整の参考情報として利用可能 + +### 2. 設定可能なパラメータ +- **IoU閾値**:0.0〜1.0(デフォルト:0.5) +- **最大マッチ数**:1〜10(デフォルト:1) +- **IoU計算方法**:bbox/polygon(デフォルト:bbox) + +### 3. 視覚的フィードバック +- マッチしたアノテーションのハイライト表示 +- IoU値の表示 +- TP/FP/FN別の色分け表示 + +## 実装の参照先 + +詳細な実装は以下のファイルを参照: +- IoU計算ロジック:`src/utils/diffCalculator.ts` +- 比較設定管理:`src/stores/useAnnotationStore.ts` +- UI実装:`src/components/ImageViewer/ImageViewer.tsx` \ No newline at end of file diff --git a/docs/DESIGN_TEMPLATE.md b/docs/DESIGN_TEMPLATE.md new file mode 100644 index 0000000..0e69d60 --- /dev/null +++ b/docs/DESIGN_TEMPLATE.md @@ -0,0 +1,190 @@ +# [機能名] - 詳細設計書 + +## 1. 概要 + +### 1.1 目的 + + +### 1.2 背景・現状の課題 + + +### 1.3 スコープ + + +### 1.4 優先度 + + +## 2. ユーザーストーリー + +### 2.1 想定ユーザー + + +### 2.2 ユースケース + + +- シナリオ1: +- シナリオ2: +- シナリオ3: + +## 3. 機能要件 + +### 3.1 必須要件 + + +### 3.2 オプション要件 + + +### 3.3 非機能要件 + + +- パフォーマンス: +- スケーラビリティ: +- ユーザビリティ: +- アクセシビリティ: + +## 4. UI/UX設計 + +### 4.1 画面構成 + + +``` +[ASCII アートまたは図の説明] +``` + +### 4.2 インタラクション + + +### 4.3 レスポンシブ対応 + + +## 5. 技術設計 + +### 5.1 アーキテクチャ + + +### 5.2 データモデル + + +```typescript +// TypeScript インターフェース定義例 +interface ExampleData { + id: string; + // ... +} +``` + +### 5.3 API設計 + + +### 5.4 状態管理 + + +### 5.5 使用技術・ライブラリ + + +| ライブラリ | 用途 | 選定理由 | +|----------|------|---------| +| example-lib | チャート描画 | 軽量で高機能 | + +## 6. 実装計画 + +### 6.1 フェーズ分け + + +- **フェーズ1**: 基本機能(X週間) + - [ ] タスク1 + - [ ] タスク2 + +- **フェーズ2**: 拡張機能(Y週間) + - [ ] タスク3 + - [ ] タスク4 + +### 6.2 マイルストーン + + +## 7. テスト計画 + +### 7.1 ユニットテスト + + +### 7.2 統合テスト + + +### 7.3 E2Eテスト + + +### 7.4 パフォーマンステスト + + +## 8. リスクと対策 + +### 8.1 技術的リスク + + +| リスク | 影響度 | 発生確率 | 対策 | +|-------|--------|---------|------| +| 例: パフォーマンス劣化 | 高 | 中 | 仮想化実装 | + +### 8.2 スケジュールリスク + + +## 9. セキュリティ考慮事項 + +### 9.1 データ保護 + + +### 9.2 アクセス制御 + + +## 10. 国際化(i18n)対応 + +### 10.1 翻訳対象 + + +### 10.2 ローカライゼーション + + +## 11. ドキュメント計画 + +### 11.1 ユーザードキュメント + + +### 11.2 開発者ドキュメント + + +## 12. 関連情報 + +### 12.1 参考資料 + + +- [リンク1](URL) +- [リンク2](URL) + +### 12.2 関連Issue/PR + + +- Issue #XX: +- PR #YY: + +## 13. 承認・レビュー + +### 13.1 レビュアー + + +- [ ] 技術リード: @username +- [ ] UIデザイナー: @username +- [ ] プロダクトオーナー: @username + +### 13.2 承認履歴 + + +| 日付 | レビュアー | ステータス | コメント | +|------|-----------|-----------|---------| +| YYYY-MM-DD | @username | 承認 | - | + +--- + +**注意事項**: +- このテンプレートは必要に応じて項目を追加・削除してください +- 小規模な機能の場合は、一部のセクションを省略可能です +- 設計書は実装前にレビューを受けてください \ No newline at end of file diff --git a/docs/IMPLEMENTATION_SUMMARY_TEMPLATE.md b/docs/IMPLEMENTATION_SUMMARY_TEMPLATE.md new file mode 100644 index 0000000..126529e --- /dev/null +++ b/docs/IMPLEMENTATION_SUMMARY_TEMPLATE.md @@ -0,0 +1,88 @@ +# 機能: [機能名] + +## 概要 + +[この機能の概要を2-3文で説明] + +## 実装期間 + +- ブランチ: `feature/[branch-name]` +- バージョン: v[X.X.X] +- コミット数: [主要コミット数] + +## 主要機能 + +### 1. [主要機能1] + +[機能の説明] + +#### 機能 + +- [機能詳細1] +- [機能詳細2] +- [機能詳細3] + +#### 技術詳細 + +- **[技術項目1]**: [詳細説明] +- **[技術項目2]**: [詳細説明] + +### 2. [主要機能2] + +[機能の説明] + +#### 機能 + +- [機能詳細1] +- [機能詳細2] + +#### 改善点 + +- [改善点1] +- [改善点2] + +## ファイル変更 + +### 新規ファイル + +- `path/to/file1` - [ファイルの説明] +- `path/to/file2` - [ファイルの説明] +- `path/to/file3` - [ファイルの説明] + +### 変更されたファイル + +主な変更は以下のファイルに加えられました: + +- `existing/file1.ts` - [変更内容の概要] +- `existing/file2.tsx` - [変更内容の概要] +- `existing/file3.rs` - [変更内容の概要] + +## バグ修正 + +1. **[バグの概要]** + - [問題の説明と解決方法] + +2. **[バグの概要]** + - [問題の説明と解決方法] + +## テスト方法 + +この機能は以下の方法でテストできます: + +1. [テスト方法1] +2. [テスト方法2] +3. [テスト方法3] + +## 今後の改善点 + +1. [将来の改善案1] +2. [将来の改善案2] +3. [将来の改善案3] + +## 依存関係 + +新しい外部依存関係は追加されていません。既存のライブラリを使用: + +- [ライブラリ1](用途) +- [ライブラリ2](用途) +- [ライブラリ3](用途) \ No newline at end of file diff --git a/docs/dev/feature-annotation-size-histogram.md b/docs/dev/feature-annotation-size-histogram.md new file mode 100644 index 0000000..46de2dd --- /dev/null +++ b/docs/dev/feature-annotation-size-histogram.md @@ -0,0 +1,168 @@ +# 機能: アノテーションサイズヒストグラム分析機能 + +## 概要 + +COCOフォーマットのアノテーションデータに対する詳細な統計分析機能を実装。ヒストグラム、ヒートマップ、統計情報の表示機能を追加し、データ探索と傾向把握を支援する包括的な分析ツールを提供。 + +## 実装期間 + +- ブランチ: `feature/annotation-size-histogram` +- ベースコミット: f53bc770cc476191b73922819fee408193ee15e0 (develop) +- コミット数: 6個の主要コミット +- 変更ファイル数: 40ファイル +- 追加行数: 4,475行、削除行数: 1,145行 + +## 主要機能 + +### 1. ヒストグラム分析機能 + +1次元の分布分析を提供し、アノテーションの特性を可視化。 + +#### 機能 + +- 5つの分布タイプ対応(幅、高さ、面積、ポリゴン面積、アスペクト比) +- ビン数調整(5-50個) +- 線形・対数スケール切り替え +- カテゴリ別フィルタリング +- 現在画像・全画像の表示切り替え +- 詳細統計情報表示(平均、中央値、標準偏差、歪度、尖度等) +- CSVデータのクリップボードコピー機能 + +#### 技術詳細 + +- **ライブラリ**: Recharts 2.15.3を使用したプロフェッショナルなチャート描画 +- **状態管理**: Zustand(useHistogramStore)による独立した状態管理 +- **計算処理**: ポリゴン面積の正確な計算(Shoelace公式) +- **パフォーマンス**: メモ化とUseMemoによる最適化 + +### 2. ヒートマップ分析機能 + +2次元の分布分析により、複数の変数間の関係性を可視化。 + +#### 機能 + +- 4つのヒートマップタイプ(幅×高さ、中心X×Y、面積×アスペクト比、ポリゴン面積×アスペクト比) +- X/Y軸のビン数独立調整(5-50個) +- 5つのカラースケール(Viridis、Plasma、Inferno、Magma、Cividis) +- カテゴリフィルタとビューモード切り替え +- 統計サマリー表示(総数、範囲情報) +- CSVエクスポート機能 + +#### 技術詳細 + +- **ライブラリ**: @nivo/heatmap 0.99.0による高品質なヒートマップ描画 +- **データ処理**: 2次元ビン分割アルゴリズムの独自実装 +- **ポリゴン処理**: Shoelace公式によるポリゴン面積計算 +- **UI統合**: 統一されたモーダルデザインによる一貫したユーザー体験 + +### 3. 共通モーダルコンポーネント + +統計機能に共通のモーダルインターフェースを提供。 + +#### 機能 + +- 統一されたモーダルデザイン +- ブラー背景効果 +- レスポンシブサイズ対応(sm, md, lg, xl) +- アクセシビリティ対応(ESCキー、オーバーレイクリック) + +#### 改善点 + +- 従来の個別モーダル実装を統一 +- 一貫したユーザー体験の提供 +- メンテナンス性の向上 + +### 4. メニュー統合とキーボードショートカット + +統計機能への簡単なアクセスを提供。 + +#### 機能 + +- Tauriメニューシステムとの統合 +- ヒストグラム: Cmd/Ctrl+Shift+H +- ヒートマップ: Cmd/Ctrl+Shift+M +- 統計ダイアログ: Cmd/Ctrl+I(既存) + +#### 技術詳細 + +- **Rustメニュー**: src-tauri/src/menu.rsでのメニュー項目追加 +- **イベント処理**: useMenuEventsフックでのイベントリスナー統合 +- **権限設定**: Tauriケイパビリティでのメニューアクセス許可 + +## ファイル変更 + +### 新規ファイル + +#### コンポーネント +- `src/components/CommonModal/` - 共通モーダルコンポーネント +- `src/components/HistogramChart/` - ヒストグラムチャートコンポーネント +- `src/components/HistogramPanel/` - ヒストグラムパネルコンポーネント +- `src/components/HeatmapChart/` - ヒートマップチャートコンポーネント +- `src/components/HeatmapPanel/` - ヒートマップパネルコンポーネント +- `src/components/HeatmapDialog/` - ヒートマップダイアログコンポーネント + +#### 状態管理 +- `src/stores/useHistogramStore.ts` - ヒストグラム専用状態管理 +- `src/stores/useHeatmapStore.ts` - ヒートマップ専用状態管理 + +#### ユーティリティ +- `src/utils/histogram.ts` - ヒストグラム計算ロジック +- `src/utils/heatmap.ts` - ヒートマップ計算ロジック + +### 変更されたファイル + +主な変更は以下のファイルに加えられました: + +- `src/App.tsx` - 新しいダイアログ統合とメニューイベント処理 +- `src/hooks/useMenuEvents.ts` - ヒストグラム・ヒートマップメニューイベント追加 +- `src/components/ControlPanel/ControlPanel.tsx` - ヒストグラムボタンの追加 +- `src-tauri/src/menu.rs` - Rustメニューシステムでの新項目追加 +- `src/i18n/locales/*.json` - 多言語対応(日本語・英語) +- `README.md`, `README.en.md` - 統計情報の免責事項追加 + +## バグ修正 + +1. **統計ダイアログの適切な統合** + - 既存の統計ダイアログを新しいCommonModalコンポーネントに移行し、一貫性を向上 + +2. **ポリゴン面積計算の正確性** + - Shoelace公式を使用したポリゴン面積計算の実装により、セグメンテーションデータの正確な分析を実現 + +3. **CSVエクスポートの信頼性** + - ブラウザのポップアップブロッカーの影響を回避するため、クリップボードAPIとフォールバック機能を実装 + +## テスト方法 + +この機能は以下の方法でテストできます: + +1. **ヒストグラム機能**: Cmd/Ctrl+Shift+Hで起動、分布タイプとビン数を変更してグラフの動的更新を確認 +2. **ヒートマップ機能**: Cmd/Ctrl+Shift+Mで起動、ヒートマップタイプとカラースケールの変更を確認 +3. **共通機能**: カテゴリフィルタ、ビューモード切り替え、統計情報表示の動作確認 +4. **データエクスポート**: クリップボードコピー機能の動作確認 +5. **レスポンシブ**: 異なる画面サイズでのUI動作確認 + +## 今後の改善点 + +1. **テスト環境の構築**: ユニットテスト・E2Eテストの実装 +2. **パフォーマンス最適化**: 大規模データセット(10万アノテーション以上)での処理速度向上 +3. **追加統計指標**: より高度な統計分析(相関分析、クラスタリング等)の実装 +4. **エクスポート機能拡張**: PDF、PNG形式でのチャートエクスポート +5. **カスタマイズ性向上**: ユーザー定義の統計指標やチャート設定の保存機能 + +## 依存関係 + +新しい外部依存関係: + +- **@nivo/core@0.99.0** - 高品質なデータ可視化コンポーネントの基盤 +- **@nivo/heatmap@0.99.0** - ヒートマップ専用コンポーネント +- **recharts@2.15.3** - React用チャートライブラリ(ヒストグラム用) + +既存ライブラリを活用: +- **React 18** - UI基盤 +- **Zustand** - 状態管理 +- **i18next** - 国際化 +- **Tauri 2.0** - デスクトップアプリケーション基盤 + +## 免責事項 + +統計機能について、データ探索目的の参考値である旨と開発者の責任制限をREADMEに明記。学術研究や商用利用時の適切な検証の重要性を強調。 \ No newline at end of file diff --git a/docs/dev/feature-ground-truth-diff-visualization.md b/docs/dev/feature-ground-truth-diff-visualization.md new file mode 100644 index 0000000..dec40b6 --- /dev/null +++ b/docs/dev/feature-ground-truth-diff-visualization.md @@ -0,0 +1,161 @@ +# 機能: 正解データ差分可視化 + +## 概要 + +この機能は、COAV (COCO Annotation Viewer) に比較モード機能を追加し、2つのCOCOデータセット(例:正解データと予測結果)の視覚的な比較と評価指標の算出を可能にします。 + +## 実装期間 + +- ブランチ: `feature/ground-truth-diff-visualization` +- バージョン: v1.x.x +- コミット数: 5つの主要コミット + +## 主要機能 + +### 1. 比較モード + +2つのCOCO形式のデータセットを並べて視覚的に比較できる新しいモードです。 + +#### 機能 + +- 2つのCOCO JSONファイルの読み込みと比較 +- TP(真陽性)、FP(偽陽性)、FN(偽陰性)アノテーションの視覚的な区別 +- IoUベースのマッチングアルゴリズム +- データセット間で異なる画像IDのサポート +- カテゴリマッピング機能 + +#### 技術詳細 + +- **マッチングアルゴリズム**: IoU閾値を使用したグリーディアルゴリズム +- **IoU計算方法**: + - バウンディングボックスIoU: 正確な面積計算 + - ポリゴンIoU: グリッドベースの近似(100x100グリッド) +- **複数マッチングサポート**: アノテーションごとの最大マッチ数を設定可能(1からN) +- **役割の割り当て**: 読み込んだファイルに対してGT/Predの役割を柔軟に割り当て + +### 2. カテゴリマッピング + +2つのデータセット間で異なるカテゴリ体系をマッピングできます。 + +#### 機能 + +- カテゴリ名による自動マッピング +- ドラッグ&ドロップスタイルの手動マッピングUI +- 1対多のカテゴリマッピングサポート +- マッピング済み/未マッピングカテゴリの視覚的表示 + +#### 改善点 + +- useEffectの無限ループを修正 +- マッピングでのカテゴリ再利用を許可 +- 未使用カテゴリの表示を改善 + +### 3. 評価指標 + +物体検出の評価指標をリアルタイムで計算・表示します。 + +#### 算出される指標 + +- 真陽性(TP) +- 偽陽性(FP) +- 偽陰性(FN) +- Precision = TP / (TP + FP) +- Recall = TP / (TP + FN) +- F1スコア = 2 * (Precision * Recall) / (Precision + Recall) + +#### 表示場所 + +- 統計ダイアログ +- アノテーション詳細パネル(比較されたアノテーション選択時) + +### 4. 視覚的な強化 + +#### カラーコーディング + +- 正解データ: TP/FNは緑色系 +- 予測結果: TP/FPは青/赤色系 +- 設定で色をカスタマイズ可能 + +#### UI改善 + +- 長時間処理用のローディングオーバーレイ +- 比較中の進捗表示 +- ズームボタンがビューポート中心を基準にズーム +- 統計での単一画像データセットサポート + +### 5. サンプルデータ生成の強化 + +比較モードのテスト用にペアデータセットを作成できるようサンプルデータジェネレータを強化しました。 + +#### 新しいオプション + +- 設定可能なマッチ分布でペアJSONを生成 +- カテゴリ名の変更(名前に"2"を追加) +- 以下の比率を設定可能: + - 完全一致 + - 部分一致 + - 不一致(FN) + - 追加オブジェクト(FP) + +## ファイル変更 + +### 新規ファイル + +- `src/components/ComparisonDialog/` - 比較設定UI +- `src/components/LoadingOverlay/` - グローバルローディングインジケータ +- `src/types/diff.ts` - 比較用のTypeScript型定義 +- `src/utils/diff.ts` - コア比較ロジック +- `src/stores/useLoadingStore.ts` - ローディング状態管理 +- `docs/COMPARISON_MODE_EVALUATION.md` - 詳細ドキュメント + +### 変更されたファイル + +主な変更は以下のファイルに加えられました: + +- `useAnnotationStore.ts` - 比較状態管理を追加 +- `ImageViewer.tsx` - ズーム機能の改善 +- `AnnotationLayer.tsx` - TP/FP/FNの可視化 +- `StatisticsDialog.tsx` - 評価指標の表示 +- `sample_generator.rs` - ペアデータ生成 + +## バグ修正 + +1. **カテゴリマッピングリセット問題** + - マッピングリセットを引き起こすuseEffectの無限ループを修正 + - 自動初期化を防ぐための手動マッピングフラグを追加 + +2. **カテゴリ再利用の制限** + - 同じカテゴリを複数回使用する制限を削除 + - 未使用カテゴリの表示ロジックを改善 + +3. **単一画像データセットサポート** + - 単一画像データセットで比較指標が表示されない問題を修正 + - 単一画像データの場合、ビューモードを自動的に'current'に設定 + +4. **ズーム動作** + - ズームボタンがビューポート中心を基準にズームするよう修正 + +## テスト方法 + +この機能は以下の方法でテストできます: + +1. ペアJSONオプション付きのサンプルデータジェネレータ +2. 実際の正解データと予測データセット +3. 異なるカテゴリ命名スキーム + +## 今後の改善点 + +1. 比較結果のエクスポート +2. 比較履歴/セッション +3. より正確なポリゴンIoU計算 +4. 複数画像のバッチ比較 +5. 追加の評価指標(mAPなど) + +## 依存関係 + +新しい外部依存関係は追加されていません。既存のライブラリを使用: + +- React + TypeScript +- Zustand(状態管理) +- Konva.js(可視化) +- Tauri(バックエンド操作) \ No newline at end of file diff --git a/docs/dev/feature-multi-image-navigation.md b/docs/dev/feature-multi-image-navigation.md new file mode 100644 index 0000000..2b5922f --- /dev/null +++ b/docs/dev/feature-multi-image-navigation.md @@ -0,0 +1,130 @@ +# 機能: feature/multi-image-navigation + +## 概要 + +複数画像間のナビゲーション機能を実装し、フォルダ内の画像を簡単に切り替えられるようにしました。また、比較モード中でも画像間を移動でき、各画像に対応するペアアノテーションが動的に更新されます。 + +## 実装期間 + +- ブランチ: `feature/multi-image-navigation` +- バージョン: v1.1.0(予定) +- コミット数: 約4 + +## 主要機能 + +### 1. NavigationPanel コンポーネント + +新しいナビゲーションパネルを実装し、フォルダ内の画像間を移動できるようにしました。 + +#### 機能 + +- 前へ/次へボタンによる画像切り替え +- IDジャンプ機能(画像IDを直接入力して移動) +- 存在しない画像のスキップ機能 +- フォルダモードと単一画像モードの切り替え +- 画像切り替え時のズーム/パン状態の保持 + +#### 技術詳細 + +- **状態管理**: Zustand を使用した `useNavigationStore` で画像リストと現在のインデックスを管理 +- **非同期処理**: Tauri の `scan_folder` コマンドで画像ファイルを効率的にスキャン +- **パフォーマンス**: 画像切り替え時の `preserveViewState` フラグで不要な再レンダリングを防止 +- **アクセシビリティ**: キーボードショートカット(矢印キー)による操作をサポート + +### 2. 比較モードの拡張 + +比較モード中でも画像間のナビゲーションが可能になり、各画像に対応するペアアノテーションが動的に更新されます。 + +#### 機能 + +- フォルダモードでは自動的に対応する画像IDのアノテーションを使用 +- 画像切り替え時にペアアノテーションを動的に更新 +- 対応するアノテーションがない画像では情報メッセージを表示 +- 比較設定変更時も全画像のアノテーションを保持 + +#### 改善点 + +- 元の比較データ(`originalComparisonData`)を保持することで、画像切り替え時の再フィルタリングが可能に +- 比較モード中でもナビゲーション機能が使えるため、複数画像の比較が効率的に + +### 3. UI/UX の改善 + +ナビゲーション関連のUIを改善し、より直感的な操作を実現しました。 + +#### 機能 + +- ナビゲーションパネルの独立したタブ化 +- 画像切り替え時のローディングインジケーター +- 画像存在確認とエラーハンドリング +- レスポンシブデザイン対応 + +#### 改善点 + +- モバイルビューでのナビゲーションパネルの最適化 +- 画像切り替え時の選択アノテーションのクリア +- パネル設定での重複表示の解消 + +## ファイル変更 + +### 新規ファイル + +- `src/components/NavigationPanel/NavigationPanel.tsx` - ナビゲーションパネルのメインコンポーネント +- `src/components/NavigationPanel/NavigationPanel.css` - ナビゲーションパネルのスタイル定義 +- `src/components/NavigationPanel/index.tsx` - エクスポート用インデックス +- `src/stores/useNavigationStore.ts` - ナビゲーション状態管理ストア + +### 変更されたファイル + +主な変更は以下のファイルに加えられました: + +- `src-tauri/src/commands/mod.rs` - `scan_folder` コマンドの追加(画像ファイルスキャン機能) +- `src/App.tsx` - NavigationPanel の統合とタブ構造の更新 +- `src/components/ComparisonDialog/ComparisonDialog.tsx` - ナビゲーションモード対応と元データ保持機能 +- `src/stores/useAnnotationStore.ts` - `originalComparisonData` の追加と `updateComparisonForCurrentImage` の実装 +- `src/stores/useImageStore.ts` - `preserveViewState` フラグの追加 +- `src/components/ImageViewer/ImageViewer.tsx` - 画像切り替え時のビュー状態保持機能 +- `src/i18n/locales/*.json` - ナビゲーション関連の翻訳文字列追加 + +## バグ修正 + +1. **ReferenceError の修正** + - 関数定義の順序を修正し、NavigationPanel の初期化エラーを解決 + +2. **画像ID リセット問題** + - タブ切り替え時の不要な画像ID リセットを防止 + +3. **ズーム/パン状態のリセット** + - 画像切り替え時に `preserveViewState` フラグを使用してビュー状態を保持 + +4. **比較モードでのデータ損失** + - 元データを保持することで、設定変更時のアノテーション損失を防止 + +5. **重複タブ表示** + - パネル設定での Info タブの重複を解消 + +## テスト方法 + +この機能は以下の方法でテストできます: + +1. フォルダを開いて複数の画像を含むアノテーションファイルを読み込む +2. ナビゲーションタブで矢印ボタンまたはキーボードの矢印キーで画像を切り替える +3. IDジャンプ機能で特定の画像IDに直接移動する +4. 比較モードを開始し、画像を切り替えてペアアノテーションが更新されることを確認 +5. 比較設定を変更後、他の画像に移動してもアノテーションが表示されることを確認 + +## 今後の改善点 + +1. サムネイルのキャッシュ機能の実装 +2. 画像のプリロード機能による切り替えの高速化 +3. フィルタリング機能(カテゴリや画像サイズでの絞り込み) +4. 画像グリッドビューの追加 +5. キーボードショートカットのカスタマイズ機能 + +## 依存関係 + +新しい外部依存関係は追加されていません。既存のライブラリを使用: + +- Zustand(状態管理) +- React(UIコンポーネント) +- Tauri API(ファイルシステムアクセス) +- i18next(国際化対応) \ No newline at end of file diff --git a/docs/prompt.md b/docs/prompt.md new file mode 100644 index 0000000..1c7669a --- /dev/null +++ b/docs/prompt.md @@ -0,0 +1,26 @@ +# AIエージェントプロンプトサンプル + +## プロンプトの目的 + +## 例 + +### 機能開発開始時 + +ISSUEから開発仕様書を作成する。 + +``` +ghコマンドで、ISSUE#{issue_number}の内容を取得し、開発仕様書を作成してください。 +開発仕様書は、docs/DESIGN_TEMPLATE.mdを参考にしてください。 +サンプルとして、docs/template/DESIGN_SAMPLE.mdを参照してください。 +``` + +### 機能開発完了時 + +開発内容をドキュメントにまとめる。 + +``` +developブランチとの差分をgitコマンドで取得し、本ブランチの変更内容を理解してください。 +変更内容をdocs/IMPLEMENTATION_SUMMARY_TEMPLATE.mdを参考にして、docs/dev/にブランチ名を付けてドキュメントを作成してください。 +ドキュメントのタイトルは、ブランチ名を使用してください。 +その後、その要点をまとめて、CHANGELOG.mdを更新してください。 +``` diff --git a/docs/template/DESIGN_SAMPLE.md b/docs/template/DESIGN_SAMPLE.md new file mode 100644 index 0000000..7ace59c --- /dev/null +++ b/docs/template/DESIGN_SAMPLE.md @@ -0,0 +1,363 @@ +# アノテーションサイズ分布ヒストグラム - 詳細設計書 + +## 1. 概要 + +### 1.1 目的 + +データセット内のオブジェクトサイズの分布を視覚的に理解し、モデル設計とデータ拡張戦略の最適化を支援する。 + +### 1.2 背景・現状の課題 + +- アノテーションサイズ分布を確認するには外部ツール(Python、Excel等)へのエクスポートが必要 +- データセットの不均衡(小さい/大きいオブジェクトの偏り)の把握が困難 +- カテゴリ別のサイズ分布比較ができない +- リアルタイムでのインタラクティブな分析が不可能 + +### 1.3 スコープ + +**対象範囲**: +- バウンディングボックスのサイズ分布可視化 +- カテゴリ別比較機能 +- インタラクティブなフィルタリング +- 統計情報の表示とエクスポート + +**対象外**: +- セグメンテーションマスクの面積計算 +- 3D アノテーションのサイズ分析 +- 時系列でのサイズ変化追跡 + +### 1.4 優先度 + +Medium - Nice to have + +## 2. ユーザーストーリー + +### 2.1 想定ユーザー + +- データサイエンティスト +- 機械学習エンジニア +- データアノテーター/キュレーター + +### 2.2 ユースケース + +- **シナリオ1**: データセットの品質評価 + - ユーザーは新しいデータセットを受け取った + - オブジェクトサイズの分布を確認して、モデル設計の参考にしたい + - 小さすぎる/大きすぎるオブジェクトの割合を把握したい + +- **シナリオ2**: データ拡張戦略の決定 + - 特定のサイズ範囲のオブジェクトが少ないことを発見 + - 該当するアノテーションを特定して、重点的に拡張したい + +- **シナリオ3**: カテゴリ間の比較分析 + - カテゴリごとのサイズ特性を比較 + - 特定カテゴリのサイズ異常を検出 + +## 3. 機能要件 + +### 3.1 必須要件 + +- **ヒストグラム表示** + - 幅、高さ、面積、アスペクト比の4種類の分布 + - ビン数の自動/手動設定(5〜100) + - リニア/ログスケールの切り替え + +- **インタラクティブ機能** + - ホバーでの詳細情報表示(ビン範囲、カウント、割合) + - クリックで該当アノテーションをハイライト + +- **カテゴリ管理** + - カテゴリ別の色分け表示 + - 特定カテゴリの表示/非表示切り替え + +### 3.2 オプション要件 + +- **高度なフィルタリング** + - 複数の条件を組み合わせたフィルタリング + - フィルタ条件の保存/読み込み + +- **比較モード** + - 複数データセットの分布比較 + - 時系列での変化追跡 + +### 3.3 非機能要件 + +- **パフォーマンス**: 10,000アノテーションで初期描画3秒以内 +- **スケーラビリティ**: 100,000アノテーションまで対応 +- **ユーザビリティ**: 3クリック以内で主要機能にアクセス +- **アクセシビリティ**: WCAG 2.1 AA準拠 + +## 4. UI/UX設計 + +### 4.1 画面構成 + +```text +┌─────────────────────────────────────────────┐ +│ アノテーションサイズ分布 │ +├─────────────────────────────────────────────┤ +│ [分布タイプ▼] [ビン数: 20 ▼] [スケール▼] │ +│ [エクスポート] │ +├─────────────────────────────────────────────┤ +│ │ +│ ▁▂▃▄▅▆▇█▇▆▅▄▃▂▁ │ +│ ヒストグラム表示エリア │ +│ │ +├─────────────────────────────────────────────┤ +│ カテゴリフィルタ: │ +│ ☑ 全て ☐ person ☐ car ☐ bicycle ... │ +├─────────────────────────────────────────────┤ +│ サイズ範囲: [====|========|====] │ +│ Min: 50px Max: 500px │ +├─────────────────────────────────────────────┤ +│ 統計情報: │ +│ 平均: 234px | 中央値: 180px | σ: 89px │ +│ 最小: 10px | 最大: 1200px │ +└─────────────────────────────────────────────┘ +``` + +### 4.2 インタラクション + +- **ホバー時**: ツールチップで詳細表示 +- **クリック時**: 対応するアノテーションを画像ビューアでハイライト +- **ドラッグ時**: 範囲選択してズーム +- **右クリック**: コンテキストメニュー(エクスポート、リセット等) + +### 4.3 レスポンシブ対応 + +- **デスクトップ** (1200px+): フル機能 +- **タブレット** (768-1199px): サイドパネル折りたたみ +- **モバイル** (< 768px): 縦スクロール、簡略表示 + +## 5. 技術設計 + +### 5.1 アーキテクチャ + +```text +┌─────────────────┐ ┌─────────────────┐ +│ HistogramPanel │────▶│ useHistogramStore│ +└─────────────────┘ └─────────────────┘ + │ │ + ▼ ▼ +┌─────────────────┐ ┌─────────────────┐ +│ HistogramChart │ │ HistogramUtils │ +└─────────────────┘ └─────────────────┘ +``` + +### 5.2 データモデル + +```typescript +interface HistogramBin { + range: [number, number]; + count: number; + categoryBreakdown: Map; + annotationIds: number[]; +} + +interface HistogramData { + type: 'width' | 'height' | 'area' | 'aspectRatio'; + bins: HistogramBin[]; + statistics: HistogramStatistics; +} + +interface HistogramStatistics { + mean: number; + median: number; + std: number; + min: number; + max: number; + q1: number; + q3: number; + total: number; +} + +interface HistogramSettings { + binCount: number; + scale: 'linear' | 'log'; + selectedCategories: Set; + sizeRange: [number, number] | null; +} +``` + +### 5.3 API設計 + +内部APIのみ(Tauriコマンドは不要) + +### 5.4 状態管理 + +```typescript +interface HistogramStore { + // 設定 + settings: HistogramSettings; + + // 計算結果 + histogramData: HistogramData | null; + highlightedAnnotations: Set; + + // アクション + setHistogramType: (type: HistogramType) => void; + setBinCount: (count: number) => void; + setScale: (scale: 'linear' | 'log') => void; + toggleCategory: (categoryId: number) => void; + setSizeRange: (range: [number, number] | null) => void; + highlightBin: (binIndex: number) => void; + exportData: (format: 'csv' | 'json') => void; +} +``` + +### 5.5 使用技術・ライブラリ + +| ライブラリ | 用途 | 選定理由 | +|-----------|------|----------| +| Recharts | チャート描画 | React対応、軽量、カスタマイズ性高 | +| d3-scale | スケール計算 | 業界標準、ログスケール対応 | +| simple-statistics | 統計計算 | 軽量、必要十分な機能 | + +## 6. 実装計画 + +### 6.1 フェーズ分け + +- **フェーズ1**: 基本機能(2週間) + - [x] ヒストグラムコンポーネントの作成 + - [x] 基本的な分布タイプ(幅、高さ、面積)の実装 + - [x] Rechartsの統合 + - [ ] 統計値計算ロジック + +- **フェーズ2**: インタラクティブ機能(1週間) + - [ ] ホバーツールチップ + - [ ] クリックでアノテーションハイライト + - [ ] カテゴリ別表示切替 + +- **フェーズ3**: フィルタリング(1週間) + - [ ] サイズ範囲スライダー + - [ ] フィルタ適用ロジック + - [ ] UIフィードバック + +- **フェーズ4**: エクスポート(3日) + - [ ] CSVエクスポート + - [ ] 画像エクスポート + +- **フェーズ5**: 最適化・テスト(3日) + - [ ] パフォーマンステスト + - [ ] UIレスポンシブ対応 + - [ ] ドキュメント作成 + +### 6.2 マイルストーン + +- M1: 基本的なヒストグラム表示(2週間後) +- M2: インタラクティブ機能完成(3週間後) +- M3: 全機能実装完了(4週間後) +- M4: リリース準備完了(5週間後) + +## 7. テスト計画 + +### 7.1 ユニットテスト + +- ヒストグラム計算関数 +- 統計値計算関数 +- ビン分割ロジック +- フィルタリング処理 + +### 7.2 統合テスト + +- Zustandストアとの連携 +- 大規模データセット(10,000+ アノテーション) +- カテゴリ切替時の再計算 + +### 7.3 E2Eテスト + +- ユーザーシナリオ1: 初回表示からエクスポートまで +- ユーザーシナリオ2: フィルタリングと分析 +- ユーザーシナリオ3: カテゴリ比較 + +### 7.4 パフォーマンステスト + +- 100,000アノテーションでの描画時間 +- メモリ使用量の監視 +- 再計算時のレスポンス + +## 8. リスクと対策 + +### 8.1 技術的リスク + +| リスク | 影響度 | 発生確率 | 対策 | +|--------|---------|----------|------| +| 大規模データでの描画遅延 | 高 | 中 | 仮想化、Web Worker使用 | +| メモリ不足 | 高 | 低 | 段階的データ読み込み | +| ライブラリ依存 | 中 | 低 | 動的インポート | + +### 8.2 スケジュールリスク + +- Rechartsのカスタマイズが想定より複雑 → D3.jsへの切り替えも検討 +- パフォーマンス要件未達 → 機能を段階的にリリース + +## 9. セキュリティ考慮事項 + +### 9.1 データ保護 + +- ローカル処理のみ(データ送信なし) +- エクスポート時のファイル権限確認 + +### 9.2 アクセス制御 + +- 読み取り専用操作のみ +- ファイルシステムへの書き込みは明示的な操作時のみ + +## 10. 国際化(i18n)対応 + +### 10.1 翻訳対象 + +- UIラベル(分布タイプ、統計情報等) +- ツールチップメッセージ +- エラーメッセージ + +### 10.2 ローカライゼーション + +- 数値フォーマット(小数点、千位区切り) +- 単位表示(px、%) + +## 11. ドキュメント計画 + +### 11.1 ユーザードキュメント + +- 機能概要と使い方 +- ヒストグラムの読み方ガイド +- FAQ + +### 11.2 開発者ドキュメント + +- コンポーネントAPI +- カスタマイズ方法 +- パフォーマンスチューニング + +## 12. 関連情報 + +### 12.1 参考資料 + +- [Recharts Documentation](https://recharts.org/) +- [D3.js Histogram Example](https://d3js.org/d3-array/bin) +- [Simple Statistics](https://simplestatistics.org/) + +### 12.2 関連Issue/PR + +- Issue #6: [FEATURE] アノテーションサイズ分布ヒストグラム + +## 13. 承認・レビュー + +### 13.1 レビュアー + +- [ ] 技術リード: @tact-software +- [ ] UIデザイナー: TBD +- [ ] プロダクトオーナー: TBD + +### 13.2 承認履歴 + +| 日付 | レビュアー | ステータス | コメント | +|------|-----------|-----------|----------| +| 2025-05-29 | - | ドラフト | 初版作成 | + +--- + +**注意事項**: +- この設計書は Issue #6 のサンプル実装例です +- 実際の実装時には、チームでのレビューと調整が必要です +- パフォーマンス要件は実際のユーザー環境に応じて調整してください \ No newline at end of file diff --git a/docs/template/IMPLEMENTATION_SUMMARY_SAMPLE.md b/docs/template/IMPLEMENTATION_SUMMARY_SAMPLE.md new file mode 100644 index 0000000..dec40b6 --- /dev/null +++ b/docs/template/IMPLEMENTATION_SUMMARY_SAMPLE.md @@ -0,0 +1,161 @@ +# 機能: 正解データ差分可視化 + +## 概要 + +この機能は、COAV (COCO Annotation Viewer) に比較モード機能を追加し、2つのCOCOデータセット(例:正解データと予測結果)の視覚的な比較と評価指標の算出を可能にします。 + +## 実装期間 + +- ブランチ: `feature/ground-truth-diff-visualization` +- バージョン: v1.x.x +- コミット数: 5つの主要コミット + +## 主要機能 + +### 1. 比較モード + +2つのCOCO形式のデータセットを並べて視覚的に比較できる新しいモードです。 + +#### 機能 + +- 2つのCOCO JSONファイルの読み込みと比較 +- TP(真陽性)、FP(偽陽性)、FN(偽陰性)アノテーションの視覚的な区別 +- IoUベースのマッチングアルゴリズム +- データセット間で異なる画像IDのサポート +- カテゴリマッピング機能 + +#### 技術詳細 + +- **マッチングアルゴリズム**: IoU閾値を使用したグリーディアルゴリズム +- **IoU計算方法**: + - バウンディングボックスIoU: 正確な面積計算 + - ポリゴンIoU: グリッドベースの近似(100x100グリッド) +- **複数マッチングサポート**: アノテーションごとの最大マッチ数を設定可能(1からN) +- **役割の割り当て**: 読み込んだファイルに対してGT/Predの役割を柔軟に割り当て + +### 2. カテゴリマッピング + +2つのデータセット間で異なるカテゴリ体系をマッピングできます。 + +#### 機能 + +- カテゴリ名による自動マッピング +- ドラッグ&ドロップスタイルの手動マッピングUI +- 1対多のカテゴリマッピングサポート +- マッピング済み/未マッピングカテゴリの視覚的表示 + +#### 改善点 + +- useEffectの無限ループを修正 +- マッピングでのカテゴリ再利用を許可 +- 未使用カテゴリの表示を改善 + +### 3. 評価指標 + +物体検出の評価指標をリアルタイムで計算・表示します。 + +#### 算出される指標 + +- 真陽性(TP) +- 偽陽性(FP) +- 偽陰性(FN) +- Precision = TP / (TP + FP) +- Recall = TP / (TP + FN) +- F1スコア = 2 * (Precision * Recall) / (Precision + Recall) + +#### 表示場所 + +- 統計ダイアログ +- アノテーション詳細パネル(比較されたアノテーション選択時) + +### 4. 視覚的な強化 + +#### カラーコーディング + +- 正解データ: TP/FNは緑色系 +- 予測結果: TP/FPは青/赤色系 +- 設定で色をカスタマイズ可能 + +#### UI改善 + +- 長時間処理用のローディングオーバーレイ +- 比較中の進捗表示 +- ズームボタンがビューポート中心を基準にズーム +- 統計での単一画像データセットサポート + +### 5. サンプルデータ生成の強化 + +比較モードのテスト用にペアデータセットを作成できるようサンプルデータジェネレータを強化しました。 + +#### 新しいオプション + +- 設定可能なマッチ分布でペアJSONを生成 +- カテゴリ名の変更(名前に"2"を追加) +- 以下の比率を設定可能: + - 完全一致 + - 部分一致 + - 不一致(FN) + - 追加オブジェクト(FP) + +## ファイル変更 + +### 新規ファイル + +- `src/components/ComparisonDialog/` - 比較設定UI +- `src/components/LoadingOverlay/` - グローバルローディングインジケータ +- `src/types/diff.ts` - 比較用のTypeScript型定義 +- `src/utils/diff.ts` - コア比較ロジック +- `src/stores/useLoadingStore.ts` - ローディング状態管理 +- `docs/COMPARISON_MODE_EVALUATION.md` - 詳細ドキュメント + +### 変更されたファイル + +主な変更は以下のファイルに加えられました: + +- `useAnnotationStore.ts` - 比較状態管理を追加 +- `ImageViewer.tsx` - ズーム機能の改善 +- `AnnotationLayer.tsx` - TP/FP/FNの可視化 +- `StatisticsDialog.tsx` - 評価指標の表示 +- `sample_generator.rs` - ペアデータ生成 + +## バグ修正 + +1. **カテゴリマッピングリセット問題** + - マッピングリセットを引き起こすuseEffectの無限ループを修正 + - 自動初期化を防ぐための手動マッピングフラグを追加 + +2. **カテゴリ再利用の制限** + - 同じカテゴリを複数回使用する制限を削除 + - 未使用カテゴリの表示ロジックを改善 + +3. **単一画像データセットサポート** + - 単一画像データセットで比較指標が表示されない問題を修正 + - 単一画像データの場合、ビューモードを自動的に'current'に設定 + +4. **ズーム動作** + - ズームボタンがビューポート中心を基準にズームするよう修正 + +## テスト方法 + +この機能は以下の方法でテストできます: + +1. ペアJSONオプション付きのサンプルデータジェネレータ +2. 実際の正解データと予測データセット +3. 異なるカテゴリ命名スキーム + +## 今後の改善点 + +1. 比較結果のエクスポート +2. 比較履歴/セッション +3. より正確なポリゴンIoU計算 +4. 複数画像のバッチ比較 +5. 追加の評価指標(mAPなど) + +## 依存関係 + +新しい外部依存関係は追加されていません。既存のライブラリを使用: + +- React + TypeScript +- Zustand(状態管理) +- Konva.js(可視化) +- Tauri(バックエンド操作) \ No newline at end of file diff --git a/package.json b/package.json index 4f2c7a7..b2d09ef 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "coav", "private": true, - "version": "1.0.0", + "version": "1.1.0", "description": "COCO Annotation Viewer - A modern desktop application for viewing and analyzing COCO dataset annotations", "license": "MIT", "author": "COAV Contributors", @@ -38,16 +38,20 @@ "version:update:sh": "bash script/update-version.sh" }, "dependencies": { + "@nivo/core": "^0.99.0", + "@nivo/heatmap": "^0.99.0", "@tauri-apps/api": "^2", "@tauri-apps/plugin-dialog": "^2.2.2", "@tauri-apps/plugin-fs": "^2.3.0", "@tauri-apps/plugin-opener": "^2", "i18next": "^25.2.1", "konva": "^9.3.20", + "lucide-react": "^0.511.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-i18next": "^15.5.2", "react-konva": "18", + "recharts": "^2.15.3", "zustand": "^5.0.5" }, "devDependencies": { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 2d4a14d..89cb3c0 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -619,7 +619,7 @@ dependencies = [ [[package]] name = "coav" -version = "1.0.0" +version = "1.1.0" dependencies = [ "chrono", "image", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 55d9f1b..1d00d68 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "coav" -version = "1.0.0" +version = "1.1.0" description = "A Tauri App" authors = ["you"] edition = "2021" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 4897a44..4620e56 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -7,9 +7,11 @@ "core:default", "opener:default", "dialog:default", + "dialog:allow-save", "fs:default", "fs:allow-read-file", "fs:allow-write-file", + "fs:allow-write-text-file", "fs:allow-create", "fs:allow-exists", "fs:allow-mkdir", diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 6247415..3889909 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -1,4 +1,5 @@ -use crate::models::COCOData; +use crate::models::{COCOData, COCOImage}; +use serde::{Deserialize, Serialize}; use std::fs; use std::path::Path; @@ -40,3 +41,137 @@ pub async fn load_image(file_path: String) -> Result, String> { Ok(image_data) } + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ImageMetadata { + pub id: i64, + pub file_name: String, + pub file_path: String, + pub width: i32, + pub height: i32, + pub file_size: Option, + pub exists: bool, + pub load_error: Option, +} + +#[tauri::command] +#[allow(dead_code)] +pub async fn scan_folder( + path: String, + coco_images: Vec, +) -> Result, String> { + let folder_path = Path::new(&path); + + // Verify folder exists + if !folder_path.exists() || !folder_path.is_dir() { + return Err(format!("Invalid folder path: {}", path)); + } + + let mut image_metadata_list = Vec::new(); + + // If no COCO images provided, scan folder for all image files + if coco_images.is_empty() { + let supported_extensions = ["jpg", "jpeg", "png", "bmp", "gif", "webp"]; + + if let Ok(entries) = fs::read_dir(folder_path) { + let mut id_counter = 1; + for entry in entries.flatten() { + let entry_path = entry.path(); + if entry_path.is_file() { + if let Some(extension) = entry_path.extension().and_then(|e| e.to_str()) { + if supported_extensions.contains(&extension.to_lowercase().as_str()) { + let file_name = entry_path + .file_name() + .and_then(|f| f.to_str()) + .unwrap_or("unknown") + .to_string(); + + let mut metadata = ImageMetadata { + id: id_counter, + file_name: file_name.clone(), + file_path: entry_path.to_string_lossy().to_string(), + width: 0, // Will be determined when image is loaded + height: 0, // Will be determined when image is loaded + file_size: None, + exists: true, + load_error: None, + }; + + // Get file size + if let Ok(file_metadata) = fs::metadata(&entry_path) { + metadata.file_size = Some(file_metadata.len()); + } + + image_metadata_list.push(metadata); + id_counter += 1; + } + } + } + } + } + + // Sort by file name for consistent ordering + image_metadata_list.sort_by(|a, b| a.file_name.cmp(&b.file_name)); + } else { + // Process each COCO image + for coco_image in coco_images { + let mut metadata = ImageMetadata { + id: coco_image.id, + file_name: coco_image.file_name.clone(), + file_path: String::new(), + width: coco_image.width, + height: coco_image.height, + file_size: None, + exists: false, + load_error: None, + }; + + // Try to find the image file in the folder + let image_path = folder_path.join(&coco_image.file_name); + + if image_path.exists() && image_path.is_file() { + metadata.file_path = image_path.to_string_lossy().to_string(); + metadata.exists = true; + + // Get file size + if let Ok(file_metadata) = fs::metadata(&image_path) { + metadata.file_size = Some(file_metadata.len()); + } + } else { + // Try to find in subdirectories + let file_name_only = Path::new(&coco_image.file_name) + .file_name() + .and_then(|f| f.to_str()) + .unwrap_or(&coco_image.file_name); + + if let Ok(entries) = fs::read_dir(folder_path) { + for entry in entries.flatten() { + if let Ok(entry_path) = entry.path().canonicalize() { + if entry_path.is_file() + && entry_path.file_name().and_then(|f| f.to_str()) + == Some(file_name_only) + { + metadata.file_path = entry_path.to_string_lossy().to_string(); + metadata.exists = true; + + if let Ok(file_metadata) = fs::metadata(&entry_path) { + metadata.file_size = Some(file_metadata.len()); + } + break; + } + } + } + } + + if !metadata.exists { + metadata.load_error = Some(format!("File not found: {}", coco_image.file_name)); + } + } + + image_metadata_list.push(metadata); + } + } + + Ok(image_metadata_list) +} diff --git a/src-tauri/src/commands/sample_generator.rs b/src-tauri/src/commands/sample_generator.rs index 6b98176..494cbf1 100644 --- a/src-tauri/src/commands/sample_generator.rs +++ b/src-tauri/src/commands/sample_generator.rs @@ -32,6 +32,13 @@ pub struct SampleGeneratorParams { pub include_licenses: Option, pub include_option: Option, pub include_multi_polygon: Option, + pub include_pair_json: Option, + pub change_pair_category_names: Option, // Whether to change category names in pair file + pub max_pair_matches: Option, // Maximum number of matching pairs per annotation (for pair generation) + pub pair_perfect_match_ratio: Option, // Ratio of perfect matches (0.0-1.0) + pub pair_partial_match_ratio: Option, // Ratio of partial matches (0.0-1.0) + pub pair_no_match_ratio: Option, // Ratio of no matches/FN (0.0-1.0) + pub pair_additional_ratio: Option, // Ratio of additional objects/FP (0.0-1.0) } #[tauri::command] @@ -366,6 +373,31 @@ pub async fn generate_sample_data(params: SampleGeneratorParams) -> Result Result COCOData { + let mut pair_annotations = Vec::new(); + let mut annotation_id_counter = 1; + + println!( + "Generating pair JSON with max_pair_matches: {}", + max_pair_matches + ); + + // Define distribution of matching patterns using the provided tuple + let (perfect_ratio, partial_ratio, no_match_ratio, additional_ratio) = distribution; + let total_annotations = original_data.annotations.len(); + let perfect_match_count = (total_annotations as f32 * perfect_ratio).round() as usize; + let partial_match_count = (total_annotations as f32 * partial_ratio).round() as usize; + let no_match_count = (total_annotations as f32 * no_match_ratio).round() as usize; + let additional_count = (total_annotations as f32 * additional_ratio).round() as usize; + + println!( + "Distribution: {} perfect, {} partial, {} no match, {} additional", + perfect_match_count, partial_match_count, no_match_count, additional_count + ); + + let mut annotation_indices: Vec = (0..original_data.annotations.len()).collect(); + use rand::seq::SliceRandom; + annotation_indices.shuffle(rng); + + // Process each original annotation according to the distribution + for (ann_idx, annotation) in original_data.annotations.iter().enumerate() { + let match_type = if ann_idx < perfect_match_count { + "perfect" + } else if ann_idx < perfect_match_count + partial_match_count { + "partial" + } else if ann_idx < perfect_match_count + partial_match_count + no_match_count { + "no_match" + } else { + "multiple" + }; + + println!("Processing annotation {} as {}", ann_idx, match_type); + + match match_type { + "perfect" => { + // Perfect match - exact copy with same category and shape + let mut pair_annotation = annotation.clone(); + pair_annotation.id = annotation_id_counter; + annotation_id_counter += 1; + pair_annotations.push(pair_annotation); + } + "partial" => { + // Partial match - shift to create partial overlap but keep same category + let mut pair_annotation = annotation.clone(); + pair_annotation.id = annotation_id_counter; + annotation_id_counter += 1; + + // Shift by 30-60% of the object size to create partial overlap + let shift_factor = rng.gen_range(0.3..0.6); + let shift_x = pair_annotation.bbox[2] + * shift_factor + * (if rng.gen_bool(0.5) { 1.0 } else { -1.0 }); + let shift_y = pair_annotation.bbox[3] + * shift_factor + * (if rng.gen_bool(0.5) { 1.0 } else { -1.0 }); + + // Calculate actual shift applied after bounds checking + let original_x = annotation.bbox[0]; + let original_y = annotation.bbox[1]; + + // Apply shift with bounds checking + pair_annotation.bbox[0] = (pair_annotation.bbox[0] + shift_x) + .max(0.0) + .min(original_data.images[0].width as f64 - pair_annotation.bbox[2]); + pair_annotation.bbox[1] = (pair_annotation.bbox[1] + shift_y) + .max(0.0) + .min(original_data.images[0].height as f64 - pair_annotation.bbox[3]); + + let actual_shift_x = pair_annotation.bbox[0] - original_x; + let actual_shift_y = pair_annotation.bbox[1] - original_y; + + // Also shift segmentation if present using actual shift amounts + if let Some(segmentation) = &mut pair_annotation.segmentation { + for polygon in segmentation.iter_mut() { + for i in (0..polygon.len()).step_by(2) { + polygon[i] += actual_shift_x; + if i + 1 < polygon.len() { + polygon[i + 1] += actual_shift_y; + } + } + } + } + + // Recalculate area after transformation + pair_annotation.area = pair_annotation.bbox[2] * pair_annotation.bbox[3]; + + pair_annotations.push(pair_annotation); + } + "no_match" => { + // No match - skip this annotation (creates FN in original) + // Do nothing, this creates a false negative + } + "multiple" => { + // For remaining annotations, create multiple matches if configured + let num_matches = if max_pair_matches > 1 && rng.gen_bool(0.3) { + rng.gen_range(2..=max_pair_matches) + } else { + 1 + }; + + println!( + "Creating {} matches for annotation {}", + num_matches, ann_idx + ); + + for match_idx in 0..num_matches { + // Multiple matches with variations + let operation = if match_idx == 0 { + 0 // First match: exact copy + } else { + 1 // Additional matches: always shift + }; + + match operation { + 0 => { + // Exact match - copy annotation as-is with same category + let mut pair_annotation = annotation.clone(); + pair_annotation.id = annotation_id_counter; + annotation_id_counter += 1; + pair_annotations.push(pair_annotation); + } + 1 => { + // Slight shift - move the annotation by a small amount but keep same category + let mut pair_annotation = annotation.clone(); + pair_annotation.id = annotation_id_counter; + annotation_id_counter += 1; + + // Shift bbox by different amounts based on match index + let base_shift = 10.0 + (match_idx as f64 * 5.0); + let shift_x = rng.gen_range(-base_shift..base_shift); + let shift_y = rng.gen_range(-base_shift..base_shift); + + // Calculate actual shift applied after bounds checking + let original_x = annotation.bbox[0]; + let original_y = annotation.bbox[1]; + + // Apply shift with bounds checking + pair_annotation.bbox[0] = + (pair_annotation.bbox[0] + shift_x).max(0.0).min( + original_data.images[0].width as f64 - pair_annotation.bbox[2], + ); + pair_annotation.bbox[1] = + (pair_annotation.bbox[1] + shift_y).max(0.0).min( + original_data.images[0].height as f64 - pair_annotation.bbox[3], + ); + + let actual_shift_x = pair_annotation.bbox[0] - original_x; + let actual_shift_y = pair_annotation.bbox[1] - original_y; + + // Also shift segmentation if present using actual shift amounts + if let Some(segmentation) = &mut pair_annotation.segmentation { + for polygon in segmentation.iter_mut() { + for i in (0..polygon.len()).step_by(2) { + polygon[i] += actual_shift_x; + if i + 1 < polygon.len() { + polygon[i + 1] += actual_shift_y; + } + } + } + } + + // Recalculate area after transformation + pair_annotation.area = + pair_annotation.bbox[2] * pair_annotation.bbox[3]; + + pair_annotations.push(pair_annotation); + } + _ => unreachable!(), + } + } + } + _ => unreachable!(), + } + } + + // Add additional annotations (FP) that don't exist in original + for _ in 0..additional_count { + // Create a new annotation at a random position + let x = rng.gen_range(50.0..original_data.images[0].width as f64 - 100.0); + let y = rng.gen_range(50.0..original_data.images[0].height as f64 - 100.0); + let width = rng.gen_range(30.0..80.0); + let height = rng.gen_range(30.0..80.0); + + let new_annotation = COCOAnnotation { + id: annotation_id_counter, + image_id: original_data.annotations[0].image_id, + // category_idはrectangle固定とする + category_id: 1, // Assuming category_id 1 is "rectangle" + bbox: vec![x, y, width, height], + area: width * height, + segmentation: Some(vec![vec![ + x, + y, + x + width, + y, + x + width, + y + height, + x, + y + height, + ]]), + iscrowd: 0, + option: None, + extra: HashMap::new(), + }; + + annotation_id_counter += 1; + pair_annotations.push(new_annotation); + } + + println!( + "Generated {} pair annotations from {} original annotations", + pair_annotations.len(), + original_data.annotations.len() + ); + + // Debug: Check for category and shape consistency + println!("Debug: Checking pair annotation consistency..."); + println!( + "Perfect matches: {}, Partial matches: {}, No matches: {}, Additional: {}", + perfect_match_count, partial_match_count, no_match_count, additional_count + ); + + let mut category_mismatches = 0; + let mut shape_mismatches = 0; + + for (i, pair_ann) in pair_annotations.iter().enumerate() { + // Only check the first perfect_match_count + partial_match_count annotations + // as they should correspond to original annotations + if i < perfect_match_count + partial_match_count && i < original_data.annotations.len() { + let original_ann = &original_data.annotations[i]; + + // Check category consistency + if pair_ann.category_id != original_ann.category_id { + category_mismatches += 1; + println!( + "Warning: Category mismatch in annotation {} - original: {}, pair: {}", + i, original_ann.category_id, pair_ann.category_id + ); + } + + // Check if both have segmentation or both don't + let original_has_seg = original_ann.segmentation.is_some(); + let pair_has_seg = pair_ann.segmentation.is_some(); + if original_has_seg != pair_has_seg { + shape_mismatches += 1; + println!( + "Warning: Shape type mismatch in annotation {} - original has segmentation: {}, pair has segmentation: {}", + i, original_has_seg, pair_has_seg + ); + } + } + } + + if category_mismatches == 0 && shape_mismatches == 0 { + println!("✓ All matched annotations have consistent categories and shapes"); + } else { + println!( + "⚠ Found {} category mismatches and {} shape mismatches", + category_mismatches, shape_mismatches + ); + } + + // Create categories with modified names if requested + let categories = if change_category_names { + original_data + .categories + .iter() + .map(|cat| COCOCategory { + id: cat.id, + name: format!("{}2", cat.name), + supercategory: cat.supercategory.clone(), + extra: cat.extra.clone(), + }) + .collect() + } else { + original_data.categories.clone() + }; + + // Create a new COCO data structure with the modified annotations + COCOData { + info: original_data.info.clone(), + images: original_data.images.clone(), + annotations: pair_annotations, + categories, + licenses: original_data.licenses.clone(), + extra: original_data.extra.clone(), + } +} + #[allow(dead_code)] fn generate_arrow_shape( rng: &mut rand::rngs::ThreadRng, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 909dd56..f3dfc7c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -2,7 +2,7 @@ mod commands; mod menu; mod models; -use commands::{load_annotations, load_image, sample_generator::generate_sample_data}; +use commands::{load_annotations, load_image, sample_generator::generate_sample_data, scan_folder}; use menu::create_menu; use tauri::{Emitter, Manager}; @@ -15,6 +15,7 @@ pub fn run() { .invoke_handler(tauri::generate_handler![ load_annotations, load_image, + scan_folder, generate_sample_data ]) .setup(|app| { diff --git a/src-tauri/src/menu.rs b/src-tauri/src/menu.rs index 64839d8..008b714 100644 --- a/src-tauri/src/menu.rs +++ b/src-tauri/src/menu.rs @@ -106,6 +106,20 @@ pub fn create_menu(app: &tauri::AppHandle) -> Result, Box(panelLayout.defaultLeftTab); - const [activeRightTab, setActiveRightTab] = useState(panelLayout.defaultRightTab); + const { selectedFolderPath, navigationMode } = useNavigationStore(); + const { isLoading, message, subMessage, progress } = useLoadingStore(); + const { imageData } = useImageStore(); + + // Check if images are available (either single image or folder) + const hasImagesAvailable = + imageData || (navigationMode === 'folder' && selectedFolderPath !== null); + + const [activeLeftTab, setActiveLeftTab] = useState(() => { + // デフォルトタブが実際に左パネルに存在するかチェック + const defaultTab = panelLayout.defaultLeftTab; + return panelLayout.leftPanelTabs.includes(defaultTab as TabType) + ? defaultTab + : panelLayout.leftPanelTabs[0] || 'control'; + }); + const [activeRightTab, setActiveRightTab] = useState(() => { + // デフォルトタブが実際に右パネルに存在するかチェック + const defaultTab = panelLayout.defaultRightTab; + return panelLayout.rightPanelTabs.includes(defaultTab as TabType) + ? defaultTab + : panelLayout.rightPanelTabs[0] || 'info'; + }); const prevPanelLayoutRef = useRef(panelLayout); const [unifiedPanelVisible, setUnifiedPanelVisible] = useState(false); const [activeUnifiedTab, setActiveUnifiedTab] = useState('control'); const [showSampleGenerator, setShowSampleGenerator] = useState(false); const [showStatistics, setShowStatistics] = useState(false); const [showImageSelection, setShowImageSelection] = useState(false); + const [showComparisonDialog, setShowComparisonDialog] = useState(false); + const [showHistogramDialog, setShowHistogramDialog] = useState(false); + const { openModal: openHeatmapModal } = useHeatmapStore(); const [tempCocoData, setTempCocoData] = useState<{ data: COCOData; annotationDir: string; @@ -64,7 +97,7 @@ function App() { const renderTabContent = (tab: string) => { switch (tab) { case 'control': - return ; + return setShowComparisonDialog(true)} />; case 'info': return ; case 'detail': @@ -74,9 +107,12 @@ function App() { ); + case 'navigation': + return ; default: return null; } @@ -146,6 +182,21 @@ function App() { ); + case 'navigation': + return ( + + + + + + ); default: return null; } @@ -162,6 +213,8 @@ function App() { return t('detail.title'); case 'files': return t('files.recentFiles'); + case 'navigation': + return t('navigation.title'); default: return ''; } @@ -268,6 +321,15 @@ function App() { const imagePath = selected as string; + // Reset to single mode when opening a single image + const { resetToSingleMode } = useNavigationStore.getState(); + + // Use setTimeout to avoid hook execution order issues + setTimeout(() => { + clearCocoData(); + resetToSingleMode(); + }, 0); + const imageBytes = await invoke('load_image', { filePath: imagePath }); const uint8Array = new Uint8Array(imageBytes); const blob = new Blob([uint8Array]); @@ -277,8 +339,6 @@ function App() { img.onload = () => { setImagePath(imagePath); setImageData(dataUrl, { width: img.width, height: img.height }); - // Clear annotations when loading a new image - clearCocoData(); // Add to recent files addRecentFile({ path: imagePath, @@ -304,6 +364,12 @@ function App() { }, [setImagePath, setImageData, setLoading, setError, clearCocoData, addRecentFile]); const handleOpenAnnotations = useCallback(async () => { + // Check if images are available + if (!hasImagesAvailable) { + toast.error(t('errors.loadAnnotationFailed'), t('errors.imageRequiredFirst')); + return; + } + try { const selected = await open({ multiple: false, @@ -332,15 +398,23 @@ function App() { // Check if there are multiple images if (data.images && data.images.length > 1) { - // Store data temporarily but don't set it in the store yet - setTempCocoData({ data, annotationDir }); - // Dialog will be shown by useEffect when tempCocoData is set + // フォルダモード(ナビゲーション有効)の場合は直接セット + if (navigationMode === 'folder') { + // フォルダ + 複数画像アノテーション → そのまま開く(ナビゲーションモード) + setCocoData(data); + // 最初の画像IDをセット + setCurrentImageId(data.images[0].id); + } else { + // 画像モード(単一画像)の場合は画像選択ダイアログ + // 画像 + 複数画像アノテーション → 画像ID選択ダイアログ + setTempCocoData({ data, annotationDir }); + } } else if (data.images && data.images.length === 1) { - // Single image, set it immediately + // 単一画像、そのまま開く setCocoData(data); setCurrentImageId(data.images[0].id); } else { - // No images, just set the data + // 画像なし、データのみセット setCocoData(data); } @@ -365,12 +439,67 @@ function App() { setError(errorMessage); toast.error(t('errors.loadAnnotationsFailed'), errorMessage); } - }, [setCocoData, setCurrentImageId, setError, addRecentFile, t]); + }, [hasImagesAvailable, setCocoData, setCurrentImageId, setError, addRecentFile, t]); const handleGenerateSample = useCallback(() => { setShowSampleGenerator(true); }, []); + const handleOpenFolder = useCallback(async () => { + try { + const selected = await open({ + directory: true, + multiple: false, + title: t('navigation.selectFolder'), + }); + + // ユーザーがキャンセルした場合 + if (!selected) { + return; // エラーではないので、何もしない + } + + if (selected) { + try { + // Clear annotations immediately when changing folder + clearCocoData(); + + // Store folder path in navigation store first + const { setSelectedFolderPath, clearImageList } = useNavigationStore.getState(); + + // Clear other state after a microtask to avoid hook issues + setTimeout(() => { + // Clear images and image list + const { clearImage } = useImageStore.getState(); + clearImage(); + clearImageList(); + + // Set the new folder path after clearing + setSelectedFolderPath(selected as string); + }, 0); + + toast.success( + 'フォルダを選択しました', + 'アノテーションを読み込むとナビゲーション機能が利用可能になります' + ); + } catch (stateError) { + console.error('Error updating state after folder selection:', stateError); + toast.error( + 'フォルダの設定中にエラーが発生しました', + stateError instanceof Error ? stateError.message : 'Unknown error' + ); + } + } + // キャンセルした場合は何もしない(エラーではない) + } catch (error) { + // ダイアログ表示時のエラーのみキャッチ + console.error('Failed to open folder dialog:', error); + toast.error( + 'フォルダ選択ダイアログの表示に失敗しました', + error instanceof Error ? error.message : 'Unknown error' + ); + } + }, [t, clearCocoData]); + const handleExportAnnotations = useCallback(() => { const { cocoData } = useAnnotationStore.getState(); if (!cocoData) return; @@ -435,6 +564,16 @@ function App() { // Reset dialog state when loading new image setShowImageSelection(false); setTempCocoData(null); + + // Reset to single mode when loading a single image + const { resetToSingleMode } = useNavigationStore.getState(); + + // Use setTimeout to avoid hook execution order issues + setTimeout(() => { + clearCocoData(); + resetToSingleMode(); + }, 0); + const imageBytes = await invoke('load_image', { filePath: imagePath }); const uint8Array = new Uint8Array(imageBytes); const blob = new Blob([uint8Array]); @@ -445,6 +584,11 @@ function App() { setImagePath(imagePath); setImageData(dataUrl, { width: img.width, height: img.height }); clearCocoData(); + + // Exit comparison mode if active + if (isComparing) { + clearComparison(); + } // Add to recent files addRecentFile({ path: imagePath, @@ -468,12 +612,27 @@ function App() { toast.error(t('errors.loadImageFailed'), errorMessage); } }, - [setImagePath, setImageData, setLoading, setError, clearCocoData, addRecentFile] + [ + setImagePath, + setImageData, + setLoading, + setError, + clearCocoData, + addRecentFile, + isComparing, + clearComparison, + ] ); // Common function to load annotations from path const loadAnnotationsFromPath = useCallback( async (jsonPath: string) => { + // Check if images are available + if (!hasImagesAvailable) { + toast.error(t('errors.loadAnnotationFailed'), t('errors.imageRequiredFirst')); + return; + } + try { // Reset previous state setShowImageSelection(false); @@ -486,17 +645,30 @@ function App() { jsonPath.substring(0, jsonPath.lastIndexOf('/')) || jsonPath.substring(0, jsonPath.lastIndexOf('\\')); + // Exit comparison mode if active + if (isComparing) { + clearComparison(); + } + // Check if there are multiple images if (data.images && data.images.length > 1) { - // Store data temporarily but don't set it in the store yet - setTempCocoData({ data, annotationDir }); - // Dialog will be shown by useEffect when tempCocoData is set + // フォルダモード(ナビゲーション有効)の場合は直接セット + if (navigationMode === 'folder') { + // フォルダ + 複数画像アノテーション → そのまま開く(ナビゲーションモード) + setCocoData(data); + // 最初の画像IDをセット + setCurrentImageId(data.images[0].id); + } else { + // 画像モード(単一画像)の場合は画像選択ダイアログ + // 画像 + 複数画像アノテーション → 画像ID選択ダイアログ + setTempCocoData({ data, annotationDir }); + } } else if (data.images && data.images.length === 1) { - // Single image, set it immediately + // 単一画像、そのまま開く setCocoData(data); setCurrentImageId(data.images[0].id); } else { - // No images, just set the data + // 画像なし、データのみセット setCocoData(data); } @@ -522,7 +694,17 @@ function App() { toast.error(t('errors.loadAnnotationsFailed'), errorMessage); } }, - [setCocoData, setError, addRecentFile, loadImageFromPath] + [ + hasImagesAvailable, + setCocoData, + setError, + addRecentFile, + loadImageFromPath, + isComparing, + clearComparison, + setCurrentImageId, + t, + ] ); // Handle recent file selection @@ -544,7 +726,10 @@ function App() { handleGenerateSample, handleExportAnnotations, handleShowStatistics, - openSettingsModal + openSettingsModal, + () => setShowComparisonDialog(true), + () => setShowHistogramDialog(true), + openHeatmapModal ); // Handle drag and drop @@ -555,7 +740,7 @@ function App() { // Auto-show right panel when annotation is selected const { selectedAnnotationIds } = useAnnotationStore(); - const prevSelectedAnnotationIdsRef = useRef([]); + const prevSelectedAnnotationIdsRef = useRef<(number | string)[]>([]); useEffect(() => { const prevLength = prevSelectedAnnotationIdsRef.current.length; @@ -624,6 +809,10 @@ function App() { e.preventDefault(); if (cocoData) handleShowStatistics(); break; + case 'k': + e.preventDefault(); + if (cocoData) setShowComparisonDialog(true); + break; } } else if (e.key === 'Escape') { e.preventDefault(); @@ -667,7 +856,7 @@ function App() {
- {['control', 'info', 'detail', 'files'].map((tab) => ( + {[...panelLayout.leftPanelTabs, ...panelLayout.rightPanelTabs].map((tab) => (
); } diff --git a/src/components/AnnotationDetailPanel/AnnotationDetailPanel.css b/src/components/AnnotationDetailPanel/AnnotationDetailPanel.css index 5061e08..e2484eb 100644 --- a/src/components/AnnotationDetailPanel/AnnotationDetailPanel.css +++ b/src/components/AnnotationDetailPanel/AnnotationDetailPanel.css @@ -128,6 +128,113 @@ margin: 0; } +/* Comparison mode styles */ +.comparison-result { + border: 2px solid var(--color-brand-primary); + border-radius: 6px; + margin-bottom: 1rem; +} + +.result-info { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.result-type, +.result-role, +.result-matches, +.result-no-match { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.result-badge { + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.75rem; + font-weight: bold; + text-transform: uppercase; +} + +.result-tp { + background: #22c55e; + color: white; +} + +.result-fp { + background: #ef4444; + color: white; +} + +.result-fn { + background: #f97316; + color: white; +} + +.role-badge { + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.75rem; + font-weight: bold; +} + +.role-badge.gt { + background: #3b82f6; + color: white; +} + +.role-badge.pred { + background: #8b5cf6; + color: white; +} + +.match-iou { + font-family: monospace; + background: var(--color-bg-secondary); + padding: 0.25rem; + border-radius: 3px; + font-size: 0.75rem; +} + +.no-match-text { + color: var(--color-text-secondary); + font-style: italic; +} + +.partner-iou { + margin-left: auto; + font-family: monospace; + font-size: 0.75rem; + background: var(--color-bg-secondary); + padding: 0.25rem 0.5rem; + border-radius: 3px; +} + +.no-partner-message { + text-align: center; + padding: 2rem; + color: var(--color-text-secondary); + font-style: italic; +} + +.annotation-basic-info { + display: flex; + flex-direction: column; + gap: 0.25rem; + margin-bottom: 1rem; +} + +.bbox-info { + margin-bottom: 1rem; + font-family: monospace; +} + +.segmentation-info { + margin-bottom: 1rem; +} + /* View mode toggle */ .detail-view-toggle { display: flex; diff --git a/src/components/AnnotationDetailPanel/AnnotationDetailPanel.tsx b/src/components/AnnotationDetailPanel/AnnotationDetailPanel.tsx index ab299d7..d454869 100644 --- a/src/components/AnnotationDetailPanel/AnnotationDetailPanel.tsx +++ b/src/components/AnnotationDetailPanel/AnnotationDetailPanel.tsx @@ -9,15 +9,217 @@ type ViewMode = 'formatted' | 'json'; const AnnotationDetailPanel: React.FC = () => { const { t } = useTranslation(); - const { selectedAnnotationIds, cocoData, getCategoryById } = useAnnotationStore(); + const { + selectedAnnotationIds, + cocoData, + getCategoryById, + isComparing, + comparisonData, + diffResults, + comparisonSettings, + } = useAnnotationStore(); const { detail } = useSettingsStore(); const [viewMode, setViewMode] = useState('formatted'); const [collapsedSections, setCollapsedSections] = useState>(new Set()); - // Get selected annotations - const selectedAnnotations = - cocoData?.annotations.filter((ann) => selectedAnnotationIds.includes(ann.id)) || []; + // Get selected annotations and comparison info + const { selectedAnnotations, comparisonInfo } = React.useMemo(() => { + if (!cocoData) return { selectedAnnotations: [], comparisonInfo: null }; + + const allAnnotations = [...cocoData.annotations]; + if (isComparing && comparisonData) { + // Add comparison annotations with source tags + const comparisonAnnotations = comparisonData.annotations.map((ann) => ({ + ...ann, + _source: 'comparison' as const, + })); + allAnnotations.push(...comparisonAnnotations); + } + + // Tag primary annotations with source (don't overwrite existing _source) + const taggedAnnotations = allAnnotations.map((ann) => ({ + ...ann, + _source: ann._source ? ann._source : ('primary' as const), + })); + + const selected = taggedAnnotations.filter((ann) => { + for (const selectedId of selectedAnnotationIds) { + if (typeof selectedId === 'string') { + // Handle unique IDs like "primary-123" or "comparison-456" + const [source, id] = selectedId.split('-'); + if (ann._source === source && ann.id === parseInt(id)) { + return true; + } + } else { + // Handle direct number IDs (non-comparison mode) + if (ann.id === selectedId) { + return true; + } + } + } + return false; + }); + + // Get comparison info for the first selected annotation + let comparison = null; + if (isComparing && selected.length > 0 && diffResults && comparisonSettings) { + const firstAnnotation = selected[0]; + + // Debug: Log selected annotation info + console.debug('Selected annotation for detail panel:', { + id: firstAnnotation.id, + source: firstAnnotation._source, + category_id: firstAnnotation.category_id, + bbox: firstAnnotation.bbox, + selectedIds: selectedAnnotationIds, + }); + + const imageDiff = diffResults.get(firstAnnotation.image_id); + + if (imageDiff) { + // Find matching pairs for this annotation + const matches = []; + + // Determine if this is GT or Pred based on source and settings + const isFromGTDataset = + (comparisonSettings.gtFileId === 'primary' && firstAnnotation._source === 'primary') || + (comparisonSettings.gtFileId === 'comparison' && + firstAnnotation._source === 'comparison'); + + // Debug: Log comparison data + console.debug('Searching for matches:', { + annotationId: firstAnnotation.id, + source: firstAnnotation._source, + isFromGTDataset, + gtFileId: comparisonSettings.gtFileId, + truePositivesCount: imageDiff.truePositives.length, + belowThresholdCount: imageDiff.belowThresholdMatches?.length || 0, + fpCount: imageDiff.falsePositives.length, + fnCount: imageDiff.falseNegatives.length, + }); + + // Check TP matches - need to match both ID and source + for (const tp of imageDiff.truePositives) { + console.debug('Checking TP match:', { + tpGtId: tp.gtAnnotation.id, + tpPredId: tp.predAnnotation.id, + searchingId: firstAnnotation.id, + isFromGTDataset, + iou: tp.iou, + }); + + if (isFromGTDataset && tp.gtAnnotation.id === firstAnnotation.id) { + // This is a GT annotation, found its TP match + console.debug('Found GT TP match!'); + matches.push({ + type: 'tp' as const, + gtAnnotation: tp.gtAnnotation, + predAnnotation: tp.predAnnotation, + iou: tp.iou, + }); + } else if (!isFromGTDataset && tp.predAnnotation.id === firstAnnotation.id) { + // This is a Pred annotation, found its TP match + console.debug('Found Pred TP match!'); + matches.push({ + type: 'tp' as const, + gtAnnotation: tp.gtAnnotation, + predAnnotation: tp.predAnnotation, + iou: tp.iou, + }); + } + } + + // Check below threshold matches for FP/FN annotations + if (matches.length === 0 && imageDiff.belowThresholdMatches) { + console.debug( + 'Checking below threshold matches:', + imageDiff.belowThresholdMatches.length + ); + for (const btm of imageDiff.belowThresholdMatches) { + console.debug('Checking below threshold match:', { + btmGtId: btm.gtAnnotation.id, + btmPredId: btm.predAnnotation.id, + searchingId: firstAnnotation.id, + isFromGTDataset, + iou: btm.iou, + }); + + if (isFromGTDataset && btm.gtAnnotation.id === firstAnnotation.id) { + // This is a GT annotation with below threshold match + console.debug('Found GT below threshold match!'); + matches.push({ + type: 'below-threshold' as const, + gtAnnotation: btm.gtAnnotation, + predAnnotation: btm.predAnnotation, + iou: btm.iou, + }); + } else if (!isFromGTDataset && btm.predAnnotation.id === firstAnnotation.id) { + // This is a Pred annotation with below threshold match + console.debug('Found Pred below threshold match!'); + matches.push({ + type: 'below-threshold' as const, + gtAnnotation: btm.gtAnnotation, + predAnnotation: btm.predAnnotation, + iou: btm.iou, + }); + } + } + } + + // Check FP (only for Pred annotations) + const isFP = + !isFromGTDataset && imageDiff.falsePositives.some((fp) => fp.id === firstAnnotation.id); + + // Check FN (only for GT annotations) + const isFN = + isFromGTDataset && imageDiff.falseNegatives.some((fn) => fn.id === firstAnnotation.id); + + // Determine annotation type + // Only consider TP if there are matches above threshold (not below-threshold matches) + const hasTPMatch = matches.some((m) => m.type === 'tp'); + + let annotationType: 'tp' | 'fp' | 'fn' = 'fp'; + if (hasTPMatch) { + annotationType = 'tp'; + } else if (isFN) { + annotationType = 'fn'; + } else if (isFP) { + annotationType = 'fp'; + } + + console.debug('Final match results:', { + annotationType, + hasTPMatch, + isFP, + isFN, + matchesFound: matches.length, + matches: matches.map((m) => ({ + type: m.type, + iou: m.iou, + gtId: m.gtAnnotation.id, + predId: m.predAnnotation.id, + })), + }); + + comparison = { + type: annotationType, + isGT: isFromGTDataset, + matches, + annotation: firstAnnotation, + }; + } + } + + return { selectedAnnotations: selected, comparisonInfo: comparison }; + }, [ + selectedAnnotationIds, + cocoData, + isComparing, + comparisonData, + diffResults, + comparisonSettings, + ]); // Toggle section collapse const toggleSection = (section: string) => { @@ -255,6 +457,12 @@ const AnnotationDetailPanel: React.FC = () => { ); } + // Show comparison sections if in comparison mode + if (isComparing && comparisonInfo) { + return renderComparisonSections(); + } + + // Single mode or non-comparison if (selectedAnnotations.length === 1) { return renderAnnotationDetails(selectedAnnotations[0]); } @@ -277,10 +485,164 @@ const AnnotationDetailPanel: React.FC = () => { ); }; + // Render comparison mode result section + const renderComparisonResultSection = () => { + if (!comparisonInfo) return null; + + const { type, isGT, matches } = comparisonInfo; + + return ( +
+
+

{t('detail.comparisonResult')}

+
+
+
+
+ {t('detail.type')}: + {type.toUpperCase()} +
+
+ {t('detail.role')}: + + {isGT ? t('detail.groundTruth') : t('detail.prediction')} + +
+ {matches.length > 0 && ( +
+ {t('detail.matches')}: + {matches.map((match, idx) => ( + + IoU: {match.iou.toFixed(3)} + {idx < matches.length - 1 && ', '} + + ))} +
+ )} + {matches.length === 0 && ( +
+ {t('detail.noMatchingPartner')} +
+ )} +
+
+
+ ); + }; + + // Render GT/Pred sections for comparison mode + const renderComparisonSections = () => { + if (!comparisonInfo) return null; + + const { matches, annotation, isGT } = comparisonInfo; + + return ( + <> + {/* Current annotation section */} +
+
+

+ {isGT ? t('detail.groundTruthAnnotation') : t('detail.predictionAnnotation')} +

+
+
+ {renderAnnotationContent(annotation as COCOAnnotation & { _source?: string })} +
+
+ + {/* Matching partner sections */} + {matches.length > 0 ? ( + matches.map((match, idx) => { + const partnerAnnotation = isGT ? match.predAnnotation : match.gtAnnotation; + const partnerLabel = isGT + ? t('detail.predictionAnnotation') + : t('detail.groundTruthAnnotation'); + + return ( +
+
+

+ {partnerLabel} {matches.length > 1 ? `(${idx + 1})` : ''} + IoU: {match.iou.toFixed(3)} +

+
+
+ {renderAnnotationContent( + partnerAnnotation as COCOAnnotation & { _source?: string } + )} +
+
+ ); + }) + ) : ( +
+
+

+ {isGT ? t('detail.predictionAnnotation') : t('detail.groundTruthAnnotation')} +

+
+
+
{t('detail.noMatchingPartner')}
+
+
+ )} + + ); + }; + + // Render annotation content (used for both single and comparison modes) + const renderAnnotationContent = (annotation: COCOAnnotation & { _source?: string }) => { + const category = getCategoryById(annotation.category_id); + + return ( + <> + {/* Basic info */} +
+
+ {t('detail.id')}: + {annotation.id} +
+
+ {t('detail.category')}: + {category?.name || annotation.category_id} +
+
+ {t('detail.area')}: + {annotation.area?.toFixed(2) || 'N/A'} +
+
+ {t('detail.iscrowd')}: + {annotation.iscrowd ? t('detail.yes') : t('detail.no')} +
+
+ + {/* Bounding box */} +
+ {t('detail.boundingBox')}: [ + {annotation.bbox.map((val: number) => val.toFixed(1)).join(', ')}] +
+ + {/* Segmentation */} + {annotation.segmentation && annotation.segmentation.length > 0 && ( +
+ {t('detail.segmentation')}: + {renderSegmentation(annotation.segmentation)} +
+ )} + + {/* Custom fields */} + {detail.promotedFields.map((field) => renderPromotedField(field, annotation))} + + ); + }; + return (
{selectedAnnotations.length > 0 && ( <> + {/* Show comparison result section if in comparison mode */} + {isComparing && comparisonInfo && renderComparisonResultSection()} +
+
+

{t('colorSettings.comparisonColors')}

+
+
+

{t('colorSettings.groundTruth')}

+
+ + + updateColorSettings({ + comparison: { + gtColors: { + tp: e.target.value, + fn: colors.comparison?.gtColors.fn || '#ff9800', + }, + predColors: colors.comparison?.predColors || { + tp: '#66bb6a', + fp: '#f44336', + }, + }, + }) + } + className="color-picker" + /> +
+
+ + + updateColorSettings({ + comparison: { + gtColors: { + tp: colors.comparison?.gtColors.tp || '#4caf50', + fn: e.target.value, + }, + predColors: colors.comparison?.predColors || { + tp: '#66bb6a', + fp: '#f44336', + }, + }, + }) + } + className="color-picker" + /> +
+
+
+

{t('colorSettings.prediction')}

+
+ + + updateColorSettings({ + comparison: { + gtColors: colors.comparison?.gtColors || { + tp: '#4caf50', + fn: '#ff9800', + }, + predColors: { + tp: e.target.value, + fp: colors.comparison?.predColors.fp || '#f44336', + }, + }, + }) + } + className="color-picker" + /> +
+
+ + + updateColorSettings({ + comparison: { + gtColors: colors.comparison?.gtColors || { + tp: '#4caf50', + fn: '#ff9800', + }, + predColors: { + tp: colors.comparison?.predColors.tp || '#66bb6a', + fp: e.target.value, + }, + }, + }) + } + className="color-picker" + /> +
+
+
+
+
+
+ +
{children}
+ + {footer &&
{footer}
} +
+
+ ); + + // React Portalを使用してbody直下にレンダリング + return createPortal(modalContent, document.body); +}; diff --git a/src/components/CommonModal/index.tsx b/src/components/CommonModal/index.tsx new file mode 100644 index 0000000..ed79164 --- /dev/null +++ b/src/components/CommonModal/index.tsx @@ -0,0 +1 @@ +export { CommonModal } from './CommonModal'; diff --git a/src/components/ComparisonDialog/ComparisonDialog.css b/src/components/ComparisonDialog/ComparisonDialog.css new file mode 100644 index 0000000..917ffae --- /dev/null +++ b/src/components/ComparisonDialog/ComparisonDialog.css @@ -0,0 +1,595 @@ +.comparison-dialog-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.comparison-dialog { + background: var(--color-bg-primary); + border-radius: 8px; + width: 90%; + max-width: 600px; + max-height: 80vh; + display: flex; + flex-direction: column; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); +} + +.comparison-dialog-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px; + border-bottom: 1px solid var(--color-border-primary); +} + +.comparison-dialog-header h2 { + margin: 0; + font-size: 1.5rem; + color: var(--color-text-primary); +} + +.close-button { + background: none; + border: none; + font-size: 2rem; + color: var(--color-text-secondary); + cursor: pointer; + padding: 0; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + transition: background-color 0.2s; +} + +.close-button:hover { + background-color: var(--color-bg-tertiary); +} + +.comparison-dialog-content { + flex: 1; + overflow-y: auto; + padding: 20px; +} + +.comparison-section { + margin-bottom: 24px; +} + +.comparison-section h3 { + margin: 0 0 12px 0; + font-size: 1.1rem; + color: var(--color-text-primary); +} + +.file-selection { + display: flex; + flex-direction: column; + gap: 8px; +} + +.file-select-button { + padding: 12px 20px; + background: var(--color-brand-primary); + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 1rem; + transition: background-color 0.2s; +} + +.file-select-button:hover:not(:disabled) { + background: var(--color-brand-primary-dark); +} + +.file-select-button:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.file-info { + font-size: 0.9rem; + color: var(--color-text-secondary); +} + +.selected-image-info { + margin-top: 4px; + font-size: 0.85rem; + color: var(--color-text-tertiary); +} + +/* Image Selection Dropdown */ +.image-selection { + display: flex; + flex-direction: column; + gap: 8px; +} + +.image-select-dropdown { + padding: 8px 12px; + border: 1px solid var(--color-border-secondary); + border-radius: 4px; + background: var(--color-bg-secondary); + color: var(--color-text-primary); + font-size: 0.9rem; + cursor: pointer; + transition: border-color 0.2s; +} + +.image-select-dropdown:hover { + border-color: var(--color-border-primary); +} + +.image-select-dropdown:focus { + outline: none; + border-color: var(--color-accent); + box-shadow: 0 0 0 2px rgba(var(--color-accent-rgb), 0.2); +} + +.role-selection { + display: flex; + flex-direction: column; + gap: 8px; +} + +.role-selection label { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; +} + +.category-mapping { + border: 1px solid var(--color-border-primary); + border-radius: 4px; + overflow: hidden; +} + +.mapping-header { + display: grid; + grid-template-columns: 1fr 1fr; + padding: 8px 12px; + background: var(--color-bg-tertiary); + font-weight: bold; + border-bottom: 1px solid var(--color-border-primary); +} + +.mapping-row { + display: grid; + grid-template-columns: 1fr 1fr; + padding: 8px 12px; + border-bottom: 1px solid var(--color-border-primary); + align-items: center; +} + +.mapping-row:last-child { + border-bottom: none; +} + +.category-name { + font-size: 0.9rem; +} + +.mapping-row select { + padding: 4px 8px; + border: 1px solid var(--color-border-primary); + border-radius: 4px; + background: var(--color-bg-primary); + color: var(--color-text-primary); +} + +.iou-threshold { + display: flex; + flex-direction: column; + gap: 8px; +} + +.iou-threshold label { + font-size: 0.9rem; + color: var(--color-text-primary); +} + +.iou-threshold input[type='range'] { + width: 100%; +} + +.max-matches { + display: flex; + flex-direction: column; + gap: 8px; + margin-top: 16px; +} + +.max-matches label { + font-size: 0.9rem; + color: var(--color-text-primary); +} + +.max-matches input[type='range'] { + width: 100%; +} + +.setting-description { + font-size: 0.85rem; + color: var(--color-text-secondary); + font-style: italic; +} + +.comparison-dialog-footer { + display: flex; + justify-content: flex-end; + gap: 12px; + padding: 20px; + border-top: 1px solid var(--color-border-primary); +} + +.cancel-button, +.compare-button { + padding: 8px 20px; + border: none; + border-radius: 4px; + font-size: 1rem; + cursor: pointer; + transition: background-color 0.2s; +} + +.cancel-button { + background: var(--color-bg-tertiary); + color: var(--color-text-primary); +} + +.cancel-button:hover { + background: var(--color-border-primary); +} + +.compare-button { + background: var(--color-brand-primary); + color: white; +} + +.compare-button:hover:not(:disabled) { + background: var(--color-brand-primary-dark); +} + +.compare-button:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +/* Polygon IoU Warning */ +.polygon-iou-warning { + margin-top: 8px; + padding: 8px 12px; + background: var(--color-warning-bg, #fef3cd); + color: var(--color-warning-text, #856404); + border: 1px solid var(--color-warning-border, #ffeaa7); + border-radius: 4px; + font-size: 0.85rem; + display: flex; + align-items: center; + gap: 8px; +} + +.polygon-iou-warning .warning-icon { + font-style: normal; + font-size: 1rem; +} + +/* Color Settings */ +.color-settings { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; +} + +.color-group h4 { + margin: 0 0 12px 0; + font-size: 0.95rem; + color: var(--color-text-secondary); +} + +.color-item { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; +} + +.color-item label { + font-size: 0.9rem; + color: var(--color-text-primary); +} + +.color-item input[type='color'] { + width: 40px; + height: 30px; + border: 1px solid var(--color-border-primary); + border-radius: 4px; +} + +/* Category Mapping One-to-Many Styles */ +.category-mapping-description { + font-size: 0.9rem; + color: var(--color-text-secondary); + margin-bottom: 12px; + padding: 8px 12px; + background: var(--color-bg-secondary); + border-radius: 4px; + border-left: 3px solid var(--color-accent); +} + +.mapping-row { + display: flex; + align-items: flex-start; + gap: 10px; + margin-bottom: 12px; +} + +.mapping-row .category-name { + flex: 0 0 200px; + font-weight: 500; + padding-top: 4px; +} + +.category-badge-list { + flex: 1; + display: flex; + flex-wrap: wrap; + gap: 6px; + align-items: center; + min-height: 32px; +} + +.category-badge { + display: inline-flex !important; + align-items: center !important; + gap: 3px !important; + padding: 4px 8px !important; + border-radius: 12px !important; + font-size: 0.8rem !important; + font-weight: 500 !important; + border: none !important; + cursor: default !important; + transition: all 0.2s ease !important; + background: var(--color-bg-secondary, #f5f5f5) !important; + color: var(--color-text-primary, #333) !important; +} + +/* 予測カテゴリ(比較ファイル) */ +.category-badge.pred-category { + background: var(--color-accent-bg, #e8f4fd) !important; + color: var(--color-accent-fg, #1976d2) !important; + border: 1px solid var(--color-accent, #2196f3) !important; +} + +/* より具体的なセレクタでバッジスタイルを強制 */ +.category-mapping .category-badge.pred-category { + background: #e3f2fd; + color: #1565c0; + border: 1px solid #42a5f5; +} + +/* 未割当比較ファイルカテゴリの行 */ +.unassigned-comparison-row { + margin-top: 16px !important; + padding-top: 12px !important; + border-top: 1px solid var(--color-border-secondary, #e0e0e0) !important; +} + +.unassigned-comparison-row .unassigned-label { + color: var(--color-text-secondary, #666) !important; + font-style: italic !important; + font-size: 0.9rem !important; +} + +/* 未割当比較ファイルカテゴリのバッジ */ +.category-badge.unassigned-comparison-category { + background: var(--color-bg-tertiary, #f9f9f9) !important; + color: var(--color-text-secondary, #666) !important; + border: 1px dashed var(--color-border-secondary, #ccc) !important; + opacity: 0.8 !important; +} + +.category-badge.unassigned-comparison-category .badge-icon { + opacity: 0.6 !important; +} + +.category-badge.add-badge { + background: var(--color-brand-primary, #1976d2); + color: white; + cursor: pointer; + border: 2px dashed rgba(255, 255, 255, 0.3); +} + +.category-badge.add-badge:hover { + background: var(--color-brand-primary-dark, #1565c0); + border-color: rgba(255, 255, 255, 0.5); + transform: translateY(-1px); +} + +.badge-icon { + font-size: 10px; + font-weight: bold; + display: flex; + align-items: center; + justify-content: center; + width: 12px; + height: 12px; +} + +.badge-text { + font-size: 0.8rem; + font-weight: 500; +} + +.badge-remove { + background: none; + border: none; + color: inherit; + cursor: pointer; + font-size: 12px; + font-weight: bold; + padding: 0; + margin-left: 2px; + width: 14px; + height: 14px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + opacity: 0.7; + transition: all 0.2s ease; +} + +.badge-remove:hover { + opacity: 1; + background: rgba(255, 255, 255, 0.2); + transform: scale(1.1); +} + +.no-mapping-text { + color: var(--color-text-secondary); + font-style: italic; + font-size: 0.85rem; +} + +/* カテゴリ選択ダイアログ */ +.category-dialog-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1100; +} + +.category-dialog { + background: var(--color-bg-primary); + border-radius: 8px; + width: 90%; + max-width: 400px; + max-height: 500px; + display: flex; + flex-direction: column; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); +} + +.category-dialog-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 20px; + border-bottom: 1px solid var(--color-border-primary); +} + +.category-dialog-header h3 { + margin: 0; + font-size: 1.2rem; + color: var(--color-text-primary); +} + +.category-dialog-content { + padding: 20px; + max-height: 350px; + overflow-y: auto; +} + +.available-categories { + display: flex; + flex-direction: column; + gap: 8px; +} + +.category-option { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 16px; + border: 1px solid var(--color-border-secondary); + border-radius: 6px; + background: var(--color-bg-secondary); + color: var(--color-text-primary); + cursor: pointer; + transition: all 0.2s ease; + font-size: 0.9rem; +} + +.category-option:hover { + background: var(--color-accent-bg); + border-color: var(--color-accent); + transform: translateY(-1px); +} + +.category-option-icon { + font-size: 14px; + color: var(--color-brand-primary); + font-weight: bold; +} + +.category-option-text { + flex: 1; + text-align: left; +} + +.no-categories { + text-align: center; + color: var(--color-text-secondary); + font-style: italic; + padding: 20px; +} + +/* Display Settings */ +.display-settings { + display: flex; + flex-direction: column; + gap: 8px; +} + +.checkbox-label { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + font-size: 0.9rem; +} + +.checkbox-label input[type='checkbox'] { + cursor: pointer; +} + +/* IoU Method Settings */ +.iou-method { + margin-top: 1rem; +} + +.radio-group { + display: flex; + gap: 1rem; + margin-top: 0.5rem; +} + +.radio-group label { + display: flex; + align-items: center; + gap: 0.5rem; + cursor: pointer; +} + +.radio-group input[type='radio'] { + cursor: pointer; +} diff --git a/src/components/ComparisonDialog/ComparisonDialog.tsx b/src/components/ComparisonDialog/ComparisonDialog.tsx new file mode 100644 index 0000000..4ccc3f7 --- /dev/null +++ b/src/components/ComparisonDialog/ComparisonDialog.tsx @@ -0,0 +1,766 @@ +import { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { open } from '@tauri-apps/plugin-dialog'; +import { invoke } from '@tauri-apps/api/core'; +import { useAnnotationStore } from '../../stores/useAnnotationStore'; +import { useNavigationStore } from '../../stores/useNavigationStore'; +import { useSettingsStore } from '../../stores/useSettingsStore'; +import { toast } from '../../stores/useToastStore'; +import { useLoadingStore } from '../../stores/useLoadingStore'; +import { CommonModal } from '../CommonModal'; +import type { COCOData } from '../../types/coco'; +import type { ComparisonSettings, DiffDisplaySettings } from '../../types/diff'; +import './ComparisonDialog.css'; + +interface Props { + isOpen: boolean; + onClose: () => void; +} + +export const ComparisonDialog = ({ isOpen, onClose }: Props) => { + const { t } = useTranslation(); + const { + cocoData, + setComparisonData: setStoreComparisonData, + currentImageId, + isComparing, + comparisonData: currentComparisonData, + originalComparisonData, + comparisonSettings: currentComparisonSettings, + } = useAnnotationStore(); + const { navigationMode } = useNavigationStore(); + const { colors: colorSettings } = useSettingsStore(); + const { setLoading } = useLoadingStore(); + + const [selectedFile, setSelectedFile] = useState(null); + const [comparisonData, setComparisonData] = useState(null); + const [selectedImageId, setSelectedImageId] = useState(null); + const [roleSelection, setRoleSelection] = useState<'current_gt' | 'current_pred'>('current_gt'); + const [iouThreshold, setIouThreshold] = useState(0.5); + const [categoryMapping, setCategoryMapping] = useState>(new Map()); + const [isLoading, setIsLoading] = useState(false); + const [maxMatchesPerAnnotation, setMaxMatchesPerAnnotation] = useState(1); + const [iouMethod, setIouMethod] = useState<'bbox' | 'polygon'>('bbox'); + const [hasManualMapping, setHasManualMapping] = useState(false); + + // Category selection dialog state + const [showCategoryDialog, setShowCategoryDialog] = useState(false); + const [currentGtCategoryId, setCurrentGtCategoryId] = useState(null); + + // Display settings + const [displaySettings, setDisplaySettings] = useState({ + showBoundingBoxes: true, + showLabels: true, + }); + + useEffect(() => { + if (isOpen) { + if (isComparing && currentComparisonData && currentComparisonSettings) { + // In comparison mode: load current settings + console.log('Loading existing comparison settings:', { + categoryMapping: currentComparisonSettings.categoryMapping, + iouThreshold: currentComparisonSettings.iouThreshold, + roleSelection: + currentComparisonSettings.gtFileId === 'primary' ? 'current_gt' : 'current_pred', + }); + + // Use original comparison data if available, otherwise use current (for backward compatibility) + setComparisonData(originalComparisonData || currentComparisonData); + setSelectedImageId(currentImageId); + setRoleSelection( + currentComparisonSettings.gtFileId === 'primary' ? 'current_gt' : 'current_pred' + ); + setIouThreshold(currentComparisonSettings.iouThreshold); + setCategoryMapping(new Map(currentComparisonSettings.categoryMapping)); + setDisplaySettings({ ...currentComparisonSettings.displaySettings }); + setMaxMatchesPerAnnotation(currentComparisonSettings.maxMatchesPerAnnotation || 1); + setIouMethod(currentComparisonSettings.iouMethod || 'bbox'); + setSelectedFile('(Current comparison file)'); // Placeholder for current file + } else { + // Not in comparison mode: clear everything + setSelectedFile(null); + setComparisonData(null); + setSelectedImageId(null); + setCategoryMapping(new Map()); + setRoleSelection('current_gt'); + setIouThreshold(0.5); + setDisplaySettings({ + showBoundingBoxes: true, + showLabels: true, + }); + setMaxMatchesPerAnnotation(1); + setIouMethod('bbox'); + setHasManualMapping(false); + } + } + }, [isOpen, isComparing, currentComparisonData, currentComparisonSettings, currentImageId]); + + useEffect(() => { + if (isOpen && cocoData && comparisonData) { + // Only initialize category mapping if we're not restoring from existing settings and no manual mapping exists + if ((!isComparing || !currentComparisonSettings) && !hasManualMapping) { + const mapping = new Map(); + cocoData.categories.forEach((cat) => { + // Try to find matching category by name + const match = comparisonData.categories.find( + (compCat) => compCat.name.toLowerCase() === cat.name.toLowerCase() + ); + if (match) { + mapping.set(cat.id, [match.id]); + } + // 割り当てなしの場合はマッピングを作成しない + }); + setCategoryMapping(mapping); + } else { + } + } + }, [isOpen, cocoData, comparisonData, isComparing, currentComparisonSettings]); + + const handleFileSelect = async () => { + try { + const selected = await open({ + filters: [ + { + name: 'COCO JSON', + extensions: ['json'], + }, + ], + }); + + if (selected) { + setIsLoading(true); + const data = await invoke('load_annotations', { + filePath: selected, + }); + + setSelectedFile(selected); + setComparisonData(data); + + // Handle image selection based on navigation mode + if (data.images && data.images.length === 1) { + // Only one image, auto-select it + setSelectedImageId(data.images[0].id); + } else if (navigationMode === 'folder' && currentImageId) { + // Folder mode: automatically find corresponding image ID + const correspondingImage = data.images.find((img) => img.id === currentImageId); + if (correspondingImage) { + setSelectedImageId(correspondingImage.id); + } else { + // No corresponding image found, show error + toast.error( + t('comparison.imageNotFound'), + t('comparison.noCorrespondingImage', { imageId: currentImageId }) + ); + setSelectedImageId(null); + } + } else { + // Single image mode with multiple images: user will select from dropdown + setSelectedImageId(null); + } + + setIsLoading(false); + } + } catch (error) { + console.error('Error loading comparison file:', error); + const errorMessage = error instanceof Error ? error.message : t('comparison.fileLoadError'); + toast.error(t('comparison.fileLoadError'), errorMessage); + setLoading(false); + } + }; + + const handleCompare = async () => { + if (!cocoData || !comparisonData || !selectedImageId) { + return; + } + + setLoading(true, t('comparison.processing'), t('comparison.processingSubMessage')); + + console.debug('Starting comparison:', { + selectedImageId, + currentImageId, + isComparing, + comparisonImages: comparisonData.images.length, + comparisonAnnotations: comparisonData.annotations.length, + }); + + // When changing settings in comparison mode, we should use the full comparison data + // Only filter for initial comparison setup + const shouldFilterData = !isComparing; + + const targetImageId = currentImageId || selectedImageId; + + const filteredComparisonData: COCOData = shouldFilterData + ? { + ...comparisonData, + // Map annotations to use the target image ID for comparison + annotations: comparisonData.annotations + .filter((ann) => ann.image_id === selectedImageId) + .map((ann) => ({ + ...ann, + image_id: targetImageId, // Use the current image ID for comparison + })), + images: comparisonData.images + .filter((img) => img.id === selectedImageId) + .map((img) => ({ + ...img, + id: targetImageId, // Use the current image ID for comparison + })), + } + : { + // In comparison mode, just update the current image's annotations + ...comparisonData, + annotations: comparisonData.annotations + .filter((ann) => ann.image_id === currentImageId!) + .map((ann) => ({ + ...ann, + image_id: currentImageId!, + })), + images: comparisonData.images + .filter((img) => img.id === currentImageId!) + .map((img) => ({ + ...img, + id: currentImageId!, + })), + }; + + console.debug('After filtering and ID mapping:', { + originalImageId: selectedImageId, + targetImageId, + currentImageId, + filteredAnnotations: filteredComparisonData.annotations.length, + filteredImages: filteredComparisonData.images.length, + firstFilteredAnnotation: filteredComparisonData.annotations[0], + }); + + const settings: ComparisonSettings = { + gtFileId: roleSelection === 'current_gt' ? 'primary' : 'comparison', + predFileId: roleSelection === 'current_gt' ? 'comparison' : 'primary', + iouThreshold, + categoryMapping: + roleSelection === 'current_gt' ? categoryMapping : invertMapping(categoryMapping), + colorSettings: colorSettings.comparison || { + gtColors: { tp: '#4caf50', fn: '#ff9800' }, + predColors: { tp: '#66bb6a', fp: '#f44336' }, + }, + displaySettings, + maxMatchesPerAnnotation: maxMatchesPerAnnotation > 1 ? maxMatchesPerAnnotation : undefined, + iouMethod, + }; + + // Always use the loaded comparison file data, not current data + // The role selection determines which is GT and which is Pred in settings + + // Use setTimeout to ensure the loading overlay appears before heavy computation + setTimeout(() => { + setStoreComparisonData(comparisonData, filteredComparisonData, settings); + setLoading(false); + onClose(); + }, 100); + }; + + const invertMapping = (mapping: Map): Map => { + const inverted = new Map(); + mapping.forEach((values, key) => { + values.forEach((value) => { + const existing = inverted.get(value) || []; + inverted.set(value, [...existing, key]); + }); + }); + return inverted; + }; + + const updateCategoryMapping = (gtCatId: number, predCatId: number, isAdd: boolean = true) => { + console.debug('updateCategoryMapping called:', { + gtCatId, + predCatId, + isAdd, + currentMappingSize: categoryMapping.size, + currentMappings: Array.from(categoryMapping.entries()), + }); + + const newMapping = new Map(categoryMapping); + const currentMappings = newMapping.get(gtCatId) || []; + + if (isAdd) { + // 追加:重複チェックして追加 + if (!currentMappings.includes(predCatId)) { + const newMappings = [...currentMappings, predCatId]; + newMapping.set(gtCatId, newMappings); + console.debug('Added mapping successfully:', { + gtCatId, + predCatId, + oldMappings: currentMappings, + newMappings, + }); + } else { + console.warn('Mapping already exists:', { gtCatId, predCatId, currentMappings }); + } + } else { + // 削除:指定されたマッピングを削除 + const filteredMappings = currentMappings.filter((id) => id !== predCatId); + if (filteredMappings.length > 0) { + newMapping.set(gtCatId, filteredMappings); + } else { + newMapping.delete(gtCatId); + } + console.debug('Removed mapping:', { + gtCatId, + predCatId, + oldMappings: currentMappings, + newMappings: filteredMappings, + }); + } + + console.debug('Setting new category mapping:', { + oldSize: categoryMapping.size, + newSize: newMapping.size, + newMappings: Array.from(newMapping.entries()), + }); + setCategoryMapping(newMapping); + setHasManualMapping(true); // Mark that manual mapping has been set + }; + + // カテゴリ選択ダイアログを開く + const openCategoryDialog = (gtCategoryId: number) => { + setCurrentGtCategoryId(gtCategoryId); + setShowCategoryDialog(true); + }; + + // カテゴリ選択ダイアログでカテゴリを選択 + const handleCategorySelect = (predCategoryId: number) => { + console.debug('handleCategorySelect called:', { + currentGtCategoryId, + predCategoryId, + currentMappingState: Array.from(categoryMapping.entries()), + }); + + if (currentGtCategoryId !== null) { + console.debug('Calling updateCategoryMapping with:', { + gtCategoryId: currentGtCategoryId, + predCategoryId, + isAdd: true, + }); + updateCategoryMapping(currentGtCategoryId, predCategoryId, true); + console.debug('After updateCategoryMapping, new state:', { + newMappingState: Array.from(categoryMapping.entries()), + }); + } else { + console.warn('currentGtCategoryId is null, cannot add mapping'); + } + setShowCategoryDialog(false); + setCurrentGtCategoryId(null); + }; + + // ダイアログが閉じられる際の処理 + const handleClose = () => { + // カテゴリ選択ダイアログも閉じる + setShowCategoryDialog(false); + setCurrentGtCategoryId(null); + onClose(); + }; + + // 表示用:使用済みでないカテゴリのみを取得 + const getUnusedCategories = () => { + if (!comparisonData) return []; + const usedCategoryIds = new Set(); + + // 現在のマッピングで使用されているカテゴリIDを収集 + categoryMapping.forEach((predIds) => { + predIds.forEach((id) => usedCategoryIds.add(id)); + }); + + // 使用されていないカテゴリのみを返す + return comparisonData.categories.filter((cat) => !usedCategoryIds.has(cat.id)); + }; + + return ( + <> + + + + + } + > + {/* Current File Info */} +
+

{t('comparison.currentFileInfo')}

+
+ {cocoData && ( + <> +
+ {t('comparison.fileInfo', { + images: cocoData.images.length, + annotations: cocoData.annotations.length, + })} +
+ {currentImageId && ( +
+ {t('comparison.currentImage')}:{' '} + {cocoData.images.find((img) => img.id === currentImageId)?.file_name || + 'Unknown'}{' '} + (ID: {currentImageId}) +
+ )} + + )} +
+
+ + {/* File Selection */} +
+

{t('comparison.fileSelection')}

+
+ + {comparisonData && ( +
+ {t('comparison.fileInfo', { + images: comparisonData.images.length, + annotations: comparisonData.annotations.length, + })} +
+ )} +
+
+ + {/* Image Selection (for multiple images in single mode only) */} + {comparisonData && comparisonData.images.length > 1 && navigationMode === 'single' && ( +
+

{t('comparison.imageSelection')}

+
+ + {selectedImageId && ( +
+ {t('comparison.selectedImageAnnotations')}:{' '} + { + comparisonData.annotations.filter((ann) => ann.image_id === selectedImageId) + .length + } +
+ )} +
+
+ )} + + {/* Show selected image info for folder mode */} + {comparisonData && navigationMode === 'folder' && selectedImageId && ( +
+

{t('comparison.selectedImage')}

+
+ {comparisonData.images.find((img) => img.id === selectedImageId)?.file_name} (ID:{' '} + {selectedImageId}) +
+ {t('comparison.selectedImageAnnotations')}:{' '} + {comparisonData.annotations.filter((ann) => ann.image_id === selectedImageId).length} +
+
+ )} + + {/* Role Selection */} +
+

{t('comparison.roleSelection')}

+
+ + +
+
+ + {/* Category Mapping */} + {cocoData && comparisonData && ( +
+

{t('comparison.categoryMapping')}

+
+ {t('comparison.categoryMappingDescription')} +
+
+
+
{t('comparison.currentFile')}
+
{t('comparison.comparisonFile')}
+
+ + {/* 割り当て済みカテゴリ */} + {cocoData.categories.map((cat) => { + const mappedCategories = categoryMapping.get(cat.id) || []; + if (mappedCategories.length === 0) return null; + + return ( +
+
+ {cat.name} ({cat.id}) +
+
+ {/* マッピングされたカテゴリのバッジ */} + {mappedCategories.map((predCatId) => { + const predCat = comparisonData.categories.find((c) => c.id === predCatId); + return ( +
+ + {predCat?.name} + +
+ ); + })} + + {/* Addバッジ */} + {getUnusedCategories().length > 0 && ( + + )} +
+
+ ); + })} + + {/* 未割当カテゴリの行追加 */} + {cocoData.categories + .filter( + (cat) => !categoryMapping.has(cat.id) || categoryMapping.get(cat.id)?.length === 0 + ) + .map((cat) => ( +
+
+ {cat.name} ({cat.id}) +
+
+ {/* 追加ボタンのみ */} + {getUnusedCategories().length > 0 && ( + + )} +
+
+ ))} + + {/* 比較ファイルの未使用カテゴリ */} + {getUnusedCategories().length > 0 && ( +
+
+ {t('comparison.availableCategories')} +
+
+ {getUnusedCategories().map((cat) => ( +
+ + {cat.name} +
+ ))} +
+
+ )} +
+
+ )} + + {/* IoU Threshold */} +
+

{t('comparison.matchingSettings')}

+
+ + setIouThreshold(parseFloat(e.target.value))} + /> +
+
+ + { + const value = parseInt(e.target.value); + if (!isNaN(value) && value >= 1) { + setMaxMatchesPerAnnotation(value); + } + }} + /> +
+
+ +
+ + +
+
{t('comparison.iouMethodDescription')}
+ {iouMethod === 'polygon' && ( +
+ ⚠️ + {t('comparison.polygonIoUWarning')} +
+ )} +
+
+ + {/* Display Settings */} +
+

{t('comparison.displaySettings')}

+
+ + +
+
+
+ + {/* カテゴリ選択ダイアログ */} + { + setShowCategoryDialog(false); + setCurrentGtCategoryId(null); + }} + title={t('comparison.selectCategory')} + size="md" + hasBlur={true} + > +
+ {getUnusedCategories().map((cat) => ( + + ))} +
+ {getUnusedCategories().length === 0 && ( +
{t('comparison.noAvailableCategories')}
+ )} +
+ + ); +}; diff --git a/src/components/ComparisonDialog/index.tsx b/src/components/ComparisonDialog/index.tsx new file mode 100644 index 0000000..16a2fd4 --- /dev/null +++ b/src/components/ComparisonDialog/index.tsx @@ -0,0 +1 @@ +export { ComparisonDialog } from './ComparisonDialog'; diff --git a/src/components/ControlPanel/ControlPanel.css b/src/components/ControlPanel/ControlPanel.css index fa88ce5..4bd54d7 100644 --- a/src/components/ControlPanel/ControlPanel.css +++ b/src/components/ControlPanel/ControlPanel.css @@ -305,3 +305,49 @@ text-transform: uppercase; font-weight: 500; } + +/* Comparison section */ +.comparison-button { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; +} + +.comparison-button .icon { + font-size: 1.2rem; +} + +.comparison-controls { + display: flex; + flex-direction: column; + gap: var(--spacing-md); +} + +.comparison-status { + padding: 8px 12px; + background: var(--color-info-bg, #e3f2fd); + color: var(--color-info-text, #1565c0); + border-radius: 4px; + font-size: 0.9rem; + text-align: center; +} + +.comparison-info { + padding: var(--spacing-xs) var(--spacing-sm); + background-color: var(--color-info-bg, #e3f2fd); + color: var(--color-info-text, #1565c0); + border-radius: var(--radius-sm); + font-size: 0.8rem; + text-align: center; +} + +.comparison-filters { + display: flex; + flex-direction: column; + gap: var(--spacing-xs); + padding: var(--spacing-sm); + background-color: var(--color-bg-secondary); + border-radius: var(--radius-md); +} diff --git a/src/components/ControlPanel/ControlPanel.tsx b/src/components/ControlPanel/ControlPanel.tsx index 265e2d3..4687cf4 100644 --- a/src/components/ControlPanel/ControlPanel.tsx +++ b/src/components/ControlPanel/ControlPanel.tsx @@ -10,7 +10,11 @@ import { extractFieldsFromAnnotation, groupFieldsByCategory } from '../../utils' import SearchBox, { SearchBoxRef } from '../SearchBox/SearchBox'; import './ControlPanel.css'; -const ControlPanel: React.FC = () => { +interface ControlPanelProps { + onOpenComparisonDialog?: () => void; +} + +const ControlPanel: React.FC = ({ onOpenComparisonDialog }) => { const { t } = useTranslation(); const searchBoxRef = useRef(null); @@ -21,6 +25,11 @@ const ControlPanel: React.FC = () => { toggleCategoryVisibility, showAllCategories, hideAllCategories, + isComparing, + clearComparison, + diffFilters, + toggleDiffFilter, + comparisonSettings, } = useAnnotationStore(); const { imageSize, zoom, zoomIn, zoomOut, resetView, fitToWindow } = useImageStore(); @@ -175,50 +184,158 @@ const ControlPanel: React.FC = () => { + {/* Comparison Section */}
-

{t('controls.categories')}

-
-
+ {!isComparing ? ( + + ) : ( +
+
{t('controls.comparingFiles')}
+ + {/* Comparison info */} +
+ {t('controls.comparisonFilterInfo')} +
+ + {/* Comparison Result Filters */} +
+