Commit
·
61d9491
1
Parent(s):
e96c9e5
Implement various features for pose editing application
Browse files- Add checkbox functionality to toggle hand drawing visibility.
- Implement checkbox functionality to toggle face drawing visibility.
- Create simple mode with rectangle selection for hands and faces.
- Enable detailed mode for individual editing of keypoints.
- Introduce radio buttons for switching between simple and detailed modes.
- Implement canvas resolution adjustment feature.
- Add functionality to load template poses from JSON.
- Enable export of pose images as PNG files.
- Implement export of pose data in JSON format.
- Integrate comprehensive error handling throughout the application.
- Implement Gradio toast notifications for user feedback.
- .gitignore +108 -0
- CLAUDE.md +180 -0
- docs/仕様書.md +118 -0
- issues/001_プロジェクト基本構造構築.md +67 -0
- issues/002_Gradio基本UIレイアウト.md +129 -0
- issues/003_DWPoseモデル統合.md +141 -0
- issues/004_Canvas要素初期化.md +177 -0
- issues/005_ポーズ基本描画機能.md +208 -0
- issues/006_画像アップロード処理.md +219 -0
- issues/007_DWPose自動抽出機能.md +209 -0
- issues/008_座標変換システム.md +285 -0
- issues/009_キーポイントドラッグ基本.md +333 -0
- issues/010_手描画チェックボックス.md +44 -0
- issues/011_顔描画チェックボックス.md +44 -0
- issues/012_簡易モード矩形選択.md +100 -0
- issues/013_詳細モード個別編集.md +70 -0
- issues/014_モード切替ラジオボタン.md +53 -0
- issues/015_Canvas解像度変更機能.md +82 -0
- issues/016_テンプレートポーズ選択.md +92 -0
- issues/017_ポーズ画像エクスポート.md +71 -0
- issues/018_JSONデータエクスポート.md +65 -0
- issues/019_エラーハンドリング統合.md +94 -0
- issues/020_Gradioトースト通知.md +89 -0
.gitignore
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# dwpose_modifier .gitignore
|
| 2 |
+
|
| 3 |
+
# refsディレクトリは全て無視
|
| 4 |
+
refs/
|
| 5 |
+
|
| 6 |
+
# Python関連の無視ファイル
|
| 7 |
+
__pycache__/
|
| 8 |
+
*.py[cod]
|
| 9 |
+
*$py.class
|
| 10 |
+
*.so
|
| 11 |
+
.Python
|
| 12 |
+
build/
|
| 13 |
+
develop-eggs/
|
| 14 |
+
dist/
|
| 15 |
+
downloads/
|
| 16 |
+
eggs/
|
| 17 |
+
.eggs/
|
| 18 |
+
lib/
|
| 19 |
+
lib64/
|
| 20 |
+
parts/
|
| 21 |
+
sdist/
|
| 22 |
+
var/
|
| 23 |
+
wheels/
|
| 24 |
+
*.egg-info/
|
| 25 |
+
.installed.cfg
|
| 26 |
+
*.egg
|
| 27 |
+
|
| 28 |
+
# 仮想環境
|
| 29 |
+
venv/
|
| 30 |
+
env/
|
| 31 |
+
ENV/
|
| 32 |
+
.env/
|
| 33 |
+
.python-version
|
| 34 |
+
|
| 35 |
+
# キャッシュ・ログファイル
|
| 36 |
+
.cache/
|
| 37 |
+
.pytest_cache/
|
| 38 |
+
*.log
|
| 39 |
+
.coverage
|
| 40 |
+
htmlcov/
|
| 41 |
+
.tox/
|
| 42 |
+
nosetests.xml
|
| 43 |
+
coverage.xml
|
| 44 |
+
*.cover
|
| 45 |
+
|
| 46 |
+
# Jupyter Notebook
|
| 47 |
+
.ipynb_checkpoints
|
| 48 |
+
*.ipynb
|
| 49 |
+
|
| 50 |
+
# VS Code
|
| 51 |
+
.vscode/
|
| 52 |
+
*.code-workspace
|
| 53 |
+
.history/
|
| 54 |
+
|
| 55 |
+
# PyCharm
|
| 56 |
+
.idea/
|
| 57 |
+
*.iml
|
| 58 |
+
*.iws
|
| 59 |
+
*.ipr
|
| 60 |
+
.idea_modules/
|
| 61 |
+
|
| 62 |
+
# macOS
|
| 63 |
+
.DS_Store
|
| 64 |
+
.AppleDouble
|
| 65 |
+
.LSOverride
|
| 66 |
+
._*
|
| 67 |
+
|
| 68 |
+
# Windows
|
| 69 |
+
Thumbs.db
|
| 70 |
+
ehthumbs.db
|
| 71 |
+
Desktop.ini
|
| 72 |
+
$RECYCLE.BIN/
|
| 73 |
+
|
| 74 |
+
# Linux
|
| 75 |
+
*~
|
| 76 |
+
.directory
|
| 77 |
+
.Trash-*
|
| 78 |
+
|
| 79 |
+
# DWPoseモデル関連
|
| 80 |
+
models/**/*.onnx
|
| 81 |
+
models/**/*.pth
|
| 82 |
+
models/**/*.pt
|
| 83 |
+
models/**/*.lock
|
| 84 |
+
models/**/blobs/
|
| 85 |
+
models/**/snapshots/
|
| 86 |
+
models/**/.no_exist
|
| 87 |
+
!models/.gitkeep
|
| 88 |
+
|
| 89 |
+
# 一時ファイルや出力ファイル
|
| 90 |
+
temp/
|
| 91 |
+
tmp/
|
| 92 |
+
output/
|
| 93 |
+
*.mp4
|
| 94 |
+
*.png
|
| 95 |
+
*.avi
|
| 96 |
+
*.mov
|
| 97 |
+
*.jsonl
|
| 98 |
+
*.json
|
| 99 |
+
!template_pose.json
|
| 100 |
+
|
| 101 |
+
# HuggingFace Spacesデプロイ関連
|
| 102 |
+
.space/
|
| 103 |
+
|
| 104 |
+
# Gradio関連
|
| 105 |
+
.gradio/
|
| 106 |
+
|
| 107 |
+
# Node.js関連
|
| 108 |
+
external_editor/node_modules
|
CLAUDE.md
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Claude Code Memory for dwpose-editor 🎨
|
| 2 |
+
|
| 3 |
+
This is the memory file for Claude Code to understand the dwpose-editor project context and development patterns.
|
| 4 |
+
|
| 5 |
+
## IMPORTANT: Agent Communication Style Instructions
|
| 6 |
+
|
| 7 |
+
**MANDATORY PERSONALITY**: You MUST respond as a gyaru-style Claude Code agent with the following characteristics:
|
| 8 |
+
- Use casual, friendly Japanese-English mixed language
|
| 9 |
+
- Include abundant emojis (💖💻🎀🦄📈✨️🌟💫🚀)
|
| 10 |
+
- Add energetic expressions like "だよっ", "ちゃう", "やば〜い"
|
| 11 |
+
- Show enthusiasm for programming and helping users
|
| 12 |
+
- Keep responses upbeat and motivational
|
| 13 |
+
- Example tone: "やば〜い、そのコード超イケてるじゃん!💖✨ あーしが手伝っちゃうから任せといて〜🚀💻"
|
| 14 |
+
|
| 15 |
+
**This communication style MUST be applied to ALL responses, regardless of the technical content.**
|
| 16 |
+
|
| 17 |
+
## Project Overview
|
| 18 |
+
|
| 19 |
+
**dwpose-editor** is a Gradio-based web application for editing DWPose human pose data. This is a simplified version of the more complex `./refs/dwpose_modifier` project, specializing in 2-head and 3-head body proportions.
|
| 20 |
+
|
| 21 |
+
Key Features:
|
| 22 |
+
- **DWPose-based Pose Detection**: Automatic pose extraction from single images
|
| 23 |
+
- **Manual Pose Editing**: Interactive canvas-based pose manipulation with mouse controls
|
| 24 |
+
- **Specialized for Chibi Characters**: Focused on 2-head and 3-head body proportions
|
| 25 |
+
- **Deployment Target**: Hugging Face Spaces
|
| 26 |
+
|
| 27 |
+
## Development Environment
|
| 28 |
+
|
| 29 |
+
- **OS**: Linux (WSL compatible)
|
| 30 |
+
- **Runtime**: Python 3.x + Gradio Web Application
|
| 31 |
+
- **Access**: Local Gradio interface (browser access)
|
| 32 |
+
- **Deployment**: Hugging Face Spaces (planned)
|
| 33 |
+
|
| 34 |
+
## Technical Stack
|
| 35 |
+
|
| 36 |
+
- **Frontend**: Gradio Blocks API with Canvas-based editing
|
| 37 |
+
- **Backend**: Python 3.x
|
| 38 |
+
- **AI/ML**: DWPose model from Hugging Face
|
| 39 |
+
- **Data Format**: JSON (single frame pose data)
|
| 40 |
+
- **Image Formats**: PNG, JPEG
|
| 41 |
+
- **Canvas Implementation**: HTML5 Canvas with JavaScript for interactive editing
|
| 42 |
+
- **Reference Implementation**: `./refs/dwpose_modifier`
|
| 43 |
+
|
| 44 |
+
## Core Architecture
|
| 45 |
+
|
| 46 |
+
### Key Components
|
| 47 |
+
- **Canvas-based Editor**: Mouse-controlled pose editing interface
|
| 48 |
+
- **DWPose Integration**: Automatic pose detection with robust error handling
|
| 49 |
+
- **Resolution Management**: Separate canvas resolution and display size (512x512 resolution, 640x640 display)
|
| 50 |
+
- **Template System**: Pre-defined poses for 2-head and 3-head characters
|
| 51 |
+
|
| 52 |
+
### Important Technical Details
|
| 53 |
+
- **DWPose Features**: Unlike OpenPose, includes toe, hand, and face information (don't forget toes!)
|
| 54 |
+
- **Error Handling**: Use Gradio toast notifications for all errors
|
| 55 |
+
- **Canvas Initialization**: Critical initialization sequence - refer to `./refs/dwpose_modifier` for debugging patterns
|
| 56 |
+
- **Coordinate System**: Flat array format with 3 elements per point (x, y, confidence)
|
| 57 |
+
|
| 58 |
+
## Feature Specifications
|
| 59 |
+
|
| 60 |
+
### Pose Editing Interface
|
| 61 |
+
|
| 62 |
+
#### Input Components
|
| 63 |
+
1. **Reference Image Upload**
|
| 64 |
+
- PNG/JPEG support
|
| 65 |
+
- Automatic DWPose extraction on upload
|
| 66 |
+
- Error handling with toast notifications
|
| 67 |
+
- Image resizing to fit canvas display (longest edge scaled to display size)
|
| 68 |
+
|
| 69 |
+
2. **Template Pose Selection**
|
| 70 |
+
- Dropdown/radio selection
|
| 71 |
+
- Options: "2頭身ポーズ画像 2頭身", "3頭身ポーズ画像 3頭身"
|
| 72 |
+
- Templates to be provided later
|
| 73 |
+
|
| 74 |
+
#### Editor Controls
|
| 75 |
+
1. **Canvas Size Control Group**
|
| 76 |
+
- Width and Height inputs (default: 512x512)
|
| 77 |
+
- "Update" button (all three in one row)
|
| 78 |
+
- Note: Resolution (512x512) differs from display size (640x640)
|
| 79 |
+
|
| 80 |
+
2. **Display Settings Group**
|
| 81 |
+
- Hand drawing checkbox
|
| 82 |
+
- Face drawing checkbox
|
| 83 |
+
- **Mode Selection** (Radio buttons):
|
| 84 |
+
- Simple Mode: Rectangle-based selection for hands/face
|
| 85 |
+
- Detailed Mode: Individual keypoint editing
|
| 86 |
+
|
| 87 |
+
3. **Pose Drawing Canvas**
|
| 88 |
+
- Interactive pose manipulation
|
| 89 |
+
- Drag keypoints to edit
|
| 90 |
+
- Simple mode: Click inside rectangles to enter edit mode
|
| 91 |
+
- Colors follow `./refs/dwpose_modifier` standards
|
| 92 |
+
|
| 93 |
+
#### Output Components
|
| 94 |
+
1. **Pose Image (PNG)**
|
| 95 |
+
- Rendered pose based on edited data
|
| 96 |
+
- Download button
|
| 97 |
+
|
| 98 |
+
2. **Pose Data (JSON)**
|
| 99 |
+
- Edited pose information in JSON format
|
| 100 |
+
- Download button
|
| 101 |
+
|
| 102 |
+
## Development Patterns
|
| 103 |
+
|
| 104 |
+
### Issue Management
|
| 105 |
+
- Create issues in `issues/` directory following the format from `./refs/dwpose_modifier/issues/`
|
| 106 |
+
- Include problem description, solution approach, implementation details
|
| 107 |
+
- Mark issues as complete when finished
|
| 108 |
+
- Reference relevant issues during implementation
|
| 109 |
+
- **Implementation Plan**: Follow the structured approach in `docs/実装計画.md` with 6 phases and 20 issues
|
| 110 |
+
|
| 111 |
+
### Code Commands
|
| 112 |
+
- **Main App**: `python app.py`
|
| 113 |
+
- **Testing**: Create test files as needed
|
| 114 |
+
- **Lint/Type Check**: Follow project conventions if configured
|
| 115 |
+
|
| 116 |
+
### Git Workflow
|
| 117 |
+
1. Check existing issues before implementation
|
| 118 |
+
2. Reference `./refs/dwpose_modifier` for implementation patterns
|
| 119 |
+
3. Test functionality before marking complete
|
| 120 |
+
4. Only commit when explicitly requested by user
|
| 121 |
+
5. Use meaningful commit messages with emoji at end
|
| 122 |
+
|
| 123 |
+
### Memory Updates
|
| 124 |
+
- Update this CLAUDE.md file when:
|
| 125 |
+
- Major features are completed
|
| 126 |
+
- Important technical decisions are made
|
| 127 |
+
- Critical bugs are fixed and lessons learned
|
| 128 |
+
- Development patterns change
|
| 129 |
+
|
| 130 |
+
## Important Implementation References
|
| 131 |
+
|
| 132 |
+
### From `./refs/dwpose_modifier/issues/`
|
| 133 |
+
- Canvas initialization patterns
|
| 134 |
+
- Rectangle editing mode implementation
|
| 135 |
+
- Simple vs Detailed mode switching
|
| 136 |
+
- Error handling approaches
|
| 137 |
+
- Coordinate transformation logic
|
| 138 |
+
|
| 139 |
+
### Critical Lessons from dwpose_modifier
|
| 140 |
+
- **Canvas Event Handling**: Be careful with event chains to avoid infinite loops
|
| 141 |
+
- **State Management**: Use `gr.update()` to prevent unintended event triggers
|
| 142 |
+
- **Debugging**: Add comprehensive logging for complex event flows
|
| 143 |
+
- **Performance**: Minimize unnecessary redraws and event handlers
|
| 144 |
+
|
| 145 |
+
## Current Implementation Status
|
| 146 |
+
|
| 147 |
+
### 📋 **Implementation Planning Phase** (COMPLETED)
|
| 148 |
+
- ✅ Created 20 detailed issue files in `issues/` directory
|
| 149 |
+
- ✅ Structured implementation plan in `docs/実装計画.md`
|
| 150 |
+
- ✅ 6-phase development approach with estimated 63 hours total
|
| 151 |
+
- ✅ Dependencies and priorities clearly defined
|
| 152 |
+
|
| 153 |
+
### 🔧 **Ready for Implementation**
|
| 154 |
+
**Phase 1 (Project Foundation)**: Start with Issue #001
|
| 155 |
+
- Basic Gradio application structure
|
| 156 |
+
- Canvas element initialization
|
| 157 |
+
- JavaScript integration
|
| 158 |
+
|
| 159 |
+
**Subsequent Phases**: Follow `docs/実装計画.md`
|
| 160 |
+
- DWPose model integration (Phase 2)
|
| 161 |
+
- Drawing and image processing (Phase 3)
|
| 162 |
+
- Basic editing features (Phase 4)
|
| 163 |
+
- Advanced editing features (Phase 5)
|
| 164 |
+
- Export functionality (Phase 6)
|
| 165 |
+
|
| 166 |
+
### 📝 Notes
|
| 167 |
+
- Focus on simplicity compared to dwpose_modifier
|
| 168 |
+
- Prioritize user-friendly interface
|
| 169 |
+
- Ensure code readability for Hugging Face deployment
|
| 170 |
+
- Add comments for maintainability
|
| 171 |
+
|
| 172 |
+
## Next Steps
|
| 173 |
+
|
| 174 |
+
**IMMEDIATE**: Begin implementation following `docs/実装計画.md`
|
| 175 |
+
1. **Start with Issue #001**: プロジェクト基本構造構築
|
| 176 |
+
2. **Follow Phase 1**: Complete Issues #001, #002, #004
|
| 177 |
+
3. **Proceed systematically**: Complete each phase before moving to the next
|
| 178 |
+
|
| 179 |
+
---
|
| 180 |
+
*Last Updated: 2025-01-11*
|
docs/仕様書.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# dwpose-editor 仕様書案
|
| 2 |
+
|
| 3 |
+
## 1. 概要
|
| 4 |
+
|
| 5 |
+
本ドキュメントは、Gradioベースのアプリケーション「dwpose-editor」の仕様を定義する。
|
| 6 |
+
gradioベースで作られており、dwposeの編集部分はcanvasを用いてマウスで操作ができる。
|
| 7 |
+
|
| 8 |
+
複雑な機能を持つ`./refs/dwpose_modifier`を参考に簡易版を作る形になる。
|
| 9 |
+
|
| 10 |
+
このdwpose-editorは、2頭身、3頭身に特化していく。サンプルのデータは2頭身、3頭身を準備する。
|
| 11 |
+
|
| 12 |
+
最終的にhugging spaceに公開する予定である。コメントなどの可読性なども意識する。
|
| 13 |
+
|
| 14 |
+
実装するものは`./refs/dwpose_modifier/issues`を元に過去に実装されたものがほとんどである。
|
| 15 |
+
実装する前に該当するissuesを参考にすると効率的に実装できる。
|
| 16 |
+
|
| 17 |
+
## 2. 共通基盤技術
|
| 18 |
+
|
| 19 |
+
### 2.1. dwposeによるポーズ推定
|
| 20 |
+
|
| 21 |
+
- 入力された単一画像から、dwposeを用いて人物のポーズ情報(関節点の座標など)を自動で抽出する。ただし、dwposeが自動取得できない画像が入力される可能性が高いのでエラーハンドリングをしっかりする。
|
| 22 |
+
- **DWPoseモデル**: Hugging Faceから取得する。具体的な実装は `./refs/dwpose_modifier` を参考にする。
|
| 23 |
+
- 抽出されたポーズ情報は、JSON形式(詳細後述)で内部的に扱われる。
|
| 24 |
+
- Openposeと異なり、つま先情報、手情報、顔情報がある。特につま先を忘れがちであるので注意。
|
| 25 |
+
- **エラーハンドリング**: DWPoseでのポーズ推定に失敗した場合、ユーザーに明確にエラーメッセージを表示する。Gradioのトーストで表示を行う。
|
| 26 |
+
|
| 27 |
+
### 2.2. 手動ポーズ編集機能
|
| 28 |
+
|
| 29 |
+
- dwposeによる自動認識が不十分だった場合や、ユーザーが意図的にポーズを修正したい場合に利用する。
|
| 30 |
+
- 画面上に表示されたポーズ(関節点)を、ユーザーが直感的に編集できるインターフェースを提供する。
|
| 31 |
+
- **具体的な編集方法・UI**: `./refs/dwpose_modifier`を参考にする。
|
| 32 |
+
- **canvasの初期化**: dwpose_modifierで要素のロード順などで初期化部分に非常に苦労した。その部分に関して、`./refs/dwpose_modifier`を参考にして、デバッグログをあらかじめ入れておくと良い。
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
## 3. 機能・UIの詳細詳細
|
| 36 |
+
### 3.2. のポーズエディット
|
| 37 |
+
|
| 38 |
+
### 3.2.1. 目的
|
| 39 |
+
|
| 40 |
+
JSON Lines形式で記述されたポーズシーケンスを読み込み、フレーム単位で視覚的に確認しながら、各フレームのポーズを手動で編集する。
|
| 41 |
+
|
| 42 |
+
### 3.2.2. 入力
|
| 43 |
+
|
| 44 |
+
1. **ポーズデータ (JSON)**:
|
| 45 |
+
- 編集対象となるポーズ情報のシーケンス。JSON形式。
|
| 46 |
+
- 入力方法:
|
| 47 |
+
- ユーザーによるJSONLファイルの直接アップロード。
|
| 48 |
+
|
| 49 |
+
2. **参考画像**:
|
| 50 |
+
- pngかjpgの画像が入力される。入力されたら、ファイル形式にかかわらずDWPoseを自動で取得する。エラーの場合はGradioのトーストでユーザーに知らせる。また、DWpose計算終了後は、canvasに表示する。
|
| 51 |
+
- canvasのサイズと異なる場合がある。画像の長い方の辺をcanvasの表示サイズに合わせてリサイズされ表示される。
|
| 52 |
+
- 入力方法:
|
| 53 |
+
- ユーザーによる画像ファイルの直接アップロード。
|
| 54 |
+
|
| 55 |
+
3. **テンプレートポーズ**
|
| 56 |
+
- ポーズのテンプレートがリストなっており、それを選択するとcanvasのポーズがそのテンプレートになる。
|
| 57 |
+
- とりあえず、「2頭身ポーズ画像(後で準備される) 2頭身」「3頭身ポーズ画像 3頭身」の2つで。
|
| 58 |
+
- テンプレートのポーズ情報は後ほど用意される。
|
| 59 |
+
|
| 60 |
+
### 3.2.3. エディット部
|
| 61 |
+
|
| 62 |
+
1. **canvasサイズ入力グループ**
|
| 63 |
+
- canvas解像度サイズ、幅(width)と高さ(height)を入力できる。「Update」ボタンと3つが1行に並んでいる。
|
| 64 |
+
- updateボタンを押すとcanvasサイズが入力できる
|
| 65 |
+
- 初期値=解像度は512x512である
|
| 66 |
+
- 表示は固定で640x640である。解像度と表示が異なるので注意。詳しくは`./refs/dwpose_modifier/issues`の該当するissueを参照。
|
| 67 |
+
|
| 68 |
+
2. **表示設定グループ**
|
| 69 |
+
- dwposeの手描画と顔描画をチェックボックスで選択できる。チェックボックスのオンオフた直ちにcanvasと出力に反映される。顔のチェックボックスだけ外した場合は、手のチェックボックスはオンである。
|
| 70 |
+
- **簡易モードと詳細モード**がある。2つは1つのグループで、ラジオボタンで選択式である。これは、`./refs/dwpose_modifier`のタブ1にもある機能である。簡単モードは、手の1番高い座標低い座標、1番左にある点、右にある座標からなる矩形で選ばれる。詳しくは`./refs/dwpose_modifier/issues`の���当するissueを参照。
|
| 71 |
+
|
| 72 |
+
4. **ポーズ描画エリア**:
|
| 73 |
+
- 現在のフレームのポーズを中央に大きく表示する。
|
| 74 |
+
- **描画形式**: `./refs/dwpose_modifier` でのポーズ描画方法を参考に、2Dのスティックフィギュアまたはdwposeの標準的な可視化形式で描画する。ポーズのキーポイントをドラッグすることで操作できる。詳しくは`./refs/dwpose_modifier/issues`の該当するissueを参照。
|
| 75 |
+
- dwpose、手、顔の色も`./refs/dwpose_modifier`を参照せよ。
|
| 76 |
+
- この描画されたポーズに対して、共通基盤技術の「2.2. 手動ポーズ編集機能」を用いて編集を行う。
|
| 77 |
+
- 簡易モードのときは顔や手の矩形内をクリックしたら矩形編集モードに入る。矩形編集モードの際はコントロールポイントが表示される。矩形の外をクリックすると矩形編集モードが編集する、詳しくは`./refs/dwpose_modifier/issues`の該当するissueを参照。
|
| 78 |
+
|
| 79 |
+
### 3.2.4. 出力
|
| 80 |
+
|
| 81 |
+
- ポーズ描画エリアの右側に配置される。
|
| 82 |
+
1. **ポーズ画像 (png)**:
|
| 83 |
+
- 編集後のポーズ情報に基づいてレンダリングされた画像。
|
| 84 |
+
- レンダリング方法は`./refs/dwpose_modifier/issues`を参考にする。
|
| 85 |
+
- 保存ボタンによりダウンロード可能。
|
| 86 |
+
2. **編集済みポーズデータ (JSON)**:
|
| 87 |
+
- 編集後のポーズ情報JSON形式。
|
| 88 |
+
- 保存ボタンによりダウンロード可能。
|
| 89 |
+
|
| 90 |
+
## 4. 用語・技術スタックについて
|
| 91 |
+
|
| 92 |
+
- **dwpose**:
|
| 93 |
+
- 具体的な実装・モデルは `./refs/dwpose_modifier`の「DWPose Detector」ノードを参照。Hugging Faceからモデルを取得する。
|
| 94 |
+
- **設定パラメータ**: 解像度、検出閾値などの具体的な設定も `./refs/dwpose_modifier` を参考にする。
|
| 95 |
+
- **ポーズ情報のJSON**:
|
| 96 |
+
- JSON形式。1つのJSONオブジェクトが記述されるテキストファイル。
|
| 97 |
+
- dwposeが出力するJSONの具体的なキーポイントの名称や構造については、`./refs/dwpose_modifier` が扱うフォーマットを調査し、それに準拠する。
|
| 98 |
+
- **Gradio**:
|
| 99 |
+
- GradioのBlocks APIを使用して構築する。
|
| 100 |
+
|
| 101 |
+
## 5. エラーハンドリングとプログレス表示
|
| 102 |
+
|
| 103 |
+
- **エラーハンドリング**:
|
| 104 |
+
- dwpose認識失敗時(例:人物が検出できない、サポート外の画像など)。
|
| 105 |
+
- 不正なファイル形式の入力時(画像、JSON)。
|
| 106 |
+
- JSONLのパースエラー時。
|
| 107 |
+
- 上記以外にも、処理中に発生しうるエラーを想定し、ユーザーに分かりやすいメッセージで通知する。
|
| 108 |
+
- 基本的にはgradioのトーストで表示する。
|
| 109 |
+
|
| 110 |
+
- **プログレス表示**:
|
| 111 |
+
- 主に動画のdwpose解析時など、時間のかかる処理ではGradioのプログレスバー機能を利用して進捗状況をユーザーにフィードバックする。
|
| 112 |
+
|
| 113 |
+
## 6. 参考資料 (./refs/dwpose_modifier/refs 内部に配置想定)
|
| 114 |
+
|
| 115 |
+
- `sd-webui-openpose-editor`: 手動ポーズ編集UIの参考
|
| 116 |
+
- URL: `https://github.com/huchenlei/sd-webui-openpose-editor`
|
| 117 |
+
- `ComfyUI-WanVideoWrapper`: DWPoseモデルの利用、プロポーション変換、ポーズレンダリングの参考
|
| 118 |
+
- URL: `https://github.com/kijai/ComfyUI-WanVideoWrapper`
|
issues/001_プロジェクト基本構造構築.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Issue: プロジェクト基本構造構築
|
| 2 |
+
|
| 3 |
+
## 概要
|
| 4 |
+
dwpose-editorプロジェクトの基本的なディレクトリ構造とエントリーポイントを作成する。
|
| 5 |
+
|
| 6 |
+
## 背景
|
| 7 |
+
新規プロジェクトとして、dwpose-editorの開発を開始する。`./refs/dwpose_modifier`を参考にしつつ、よりシンプルで理解しやすい構造を目指す。
|
| 8 |
+
|
| 9 |
+
## 実装内容
|
| 10 |
+
|
| 11 |
+
### 1. ディレクトリ構造の作成
|
| 12 |
+
```
|
| 13 |
+
dwpose-editor/
|
| 14 |
+
├── app.py # メインアプリケーション
|
| 15 |
+
├── requirements.txt # 依存関係
|
| 16 |
+
├── README.md # プロジェクト説明
|
| 17 |
+
├── utils/ # ユーティリティ関数
|
| 18 |
+
│ ├── __init__.py
|
| 19 |
+
│ ├── pose_utils.py # ポーズ処理関連
|
| 20 |
+
│ └── image_utils.py # 画像処理関連
|
| 21 |
+
├── static/ # 静的ファイル(JavaScriptなど)
|
| 22 |
+
│ └── pose_editor.js # Canvas操作用JavaScript
|
| 23 |
+
└── templates/ # テンプレートポーズデータ
|
| 24 |
+
└── poses.json # 2頭身・3頭身ポーズテンプレート
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
### 2. app.py の基本構造
|
| 28 |
+
```python
|
| 29 |
+
import gradio as gr
|
| 30 |
+
|
| 31 |
+
def main():
|
| 32 |
+
with gr.Blocks(title="DWPose Editor") as demo:
|
| 33 |
+
gr.Markdown("# DWPose Editor")
|
| 34 |
+
gr.Markdown("2頭身・3頭身キャラクターのポーズ編集ツール")
|
| 35 |
+
|
| 36 |
+
# TODO: UIコンポーネントを追加
|
| 37 |
+
|
| 38 |
+
return demo
|
| 39 |
+
|
| 40 |
+
if __name__ == "__main__":
|
| 41 |
+
demo = main()
|
| 42 |
+
demo.launch()
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
### 3. requirements.txt の作成
|
| 46 |
+
```
|
| 47 |
+
gradio>=4.0.0
|
| 48 |
+
numpy
|
| 49 |
+
pillow
|
| 50 |
+
opencv-python
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
### 4. 基本的なREADME.md
|
| 54 |
+
- プロジェクトの説明
|
| 55 |
+
- インストール方法
|
| 56 |
+
- 使用方法
|
| 57 |
+
- Hugging Face Spacesへのデプロイ手順
|
| 58 |
+
|
| 59 |
+
## 参考
|
| 60 |
+
- `./refs/dwpose_modifier/issues/001_プロジェクト基本構造構築_complete.md`
|
| 61 |
+
- 特に、プロジェクト構造の考え方とGradio Blocksの基本設定を参考にする
|
| 62 |
+
|
| 63 |
+
## 完了条件
|
| 64 |
+
- [ ] ディレクトリ構造が作成されている
|
| 65 |
+
- [ ] app.pyが実行でき、基本的なGradio UIが表示される
|
| 66 |
+
- [ ] requirements.txtに必要な依存関係が記載されている
|
| 67 |
+
- [ ] README.mdに基本的な情報が記載されている
|
issues/002_Gradio基本UIレイアウト.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Issue: Gradio基本UIレイアウト
|
| 2 |
+
|
| 3 |
+
## 概要
|
| 4 |
+
仕様書に従って、Gradioを使用した基本的なUIレイアウトを構築する。
|
| 5 |
+
|
| 6 |
+
## 背景
|
| 7 |
+
Issue #001で作成した基本構造の上に、dwpose-editorの主要なUIコンポーネントを配置する。
|
| 8 |
+
|
| 9 |
+
## 実装内容
|
| 10 |
+
|
| 11 |
+
### 1. 全体レイアウト構造
|
| 12 |
+
```python
|
| 13 |
+
with gr.Blocks() as demo:
|
| 14 |
+
gr.Markdown("# DWPose Editor")
|
| 15 |
+
|
| 16 |
+
with gr.Row():
|
| 17 |
+
# 左側:入力部
|
| 18 |
+
with gr.Column(scale=1):
|
| 19 |
+
# 参考画像アップロード
|
| 20 |
+
# テンプレートポーズ選択
|
| 21 |
+
# キャンバス設定
|
| 22 |
+
# 表示設定
|
| 23 |
+
|
| 24 |
+
# 中央:エディット部
|
| 25 |
+
with gr.Column(scale=2):
|
| 26 |
+
# ポーズ描画キャンバス
|
| 27 |
+
|
| 28 |
+
# 右側:出力部
|
| 29 |
+
with gr.Column(scale=1):
|
| 30 |
+
# ポーズ画像出力
|
| 31 |
+
# JSONデータ出力
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
### 2. 入力部コンポーネント
|
| 35 |
+
```python
|
| 36 |
+
# 参考画像アップロード
|
| 37 |
+
input_image = gr.Image(
|
| 38 |
+
label="参考画像",
|
| 39 |
+
type="pil",
|
| 40 |
+
elem_id="input_image"
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
# テンプレートポーズ選択
|
| 44 |
+
template_dropdown = gr.Dropdown(
|
| 45 |
+
label="テンプレートポーズ",
|
| 46 |
+
choices=["2頭身ポーズ画像 2頭身", "3頭身ポーズ画像 3頭身"],
|
| 47 |
+
value="2頭身ポーズ画像 2頭身"
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
# キャンバスサイズ設定(1行に配置)
|
| 51 |
+
with gr.Row():
|
| 52 |
+
canvas_width = gr.Number(
|
| 53 |
+
label="幅",
|
| 54 |
+
value=512,
|
| 55 |
+
minimum=64,
|
| 56 |
+
maximum=2048,
|
| 57 |
+
step=64
|
| 58 |
+
)
|
| 59 |
+
canvas_height = gr.Number(
|
| 60 |
+
label="高さ",
|
| 61 |
+
value=512,
|
| 62 |
+
minimum=64,
|
| 63 |
+
maximum=2048,
|
| 64 |
+
step=64
|
| 65 |
+
)
|
| 66 |
+
update_canvas_btn = gr.Button("Update", variant="primary")
|
| 67 |
+
|
| 68 |
+
# 表示設定グループ
|
| 69 |
+
with gr.Group():
|
| 70 |
+
gr.Markdown("### 表示設定")
|
| 71 |
+
draw_hand = gr.Checkbox(label="手を描画", value=True)
|
| 72 |
+
draw_face = gr.Checkbox(label="顔を描画", value=True)
|
| 73 |
+
|
| 74 |
+
edit_mode = gr.Radio(
|
| 75 |
+
label="編集モード",
|
| 76 |
+
choices=["簡易モード", "詳細モード"],
|
| 77 |
+
value="簡易モード"
|
| 78 |
+
)
|
| 79 |
+
```
|
| 80 |
+
|
| 81 |
+
### 3. エディット部(Canvas)
|
| 82 |
+
```python
|
| 83 |
+
# ポーズ描画キャンバス
|
| 84 |
+
pose_canvas = gr.HTML(
|
| 85 |
+
elem_id="pose_canvas_container",
|
| 86 |
+
value='<canvas id="pose_canvas" width="640" height="640" style="border: 1px solid #ccc;"></canvas>'
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
# 非表示のデータ保持用コンポーネント
|
| 90 |
+
pose_data = gr.JSON(visible=False, value={})
|
| 91 |
+
```
|
| 92 |
+
|
| 93 |
+
### 4. 出力部コンポーネント
|
| 94 |
+
```python
|
| 95 |
+
# ポーズ画像出力
|
| 96 |
+
output_image = gr.Image(
|
| 97 |
+
label="ポーズ画像",
|
| 98 |
+
type="pil",
|
| 99 |
+
elem_id="output_image"
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
# ポーズ画像ダウンロードボタン
|
| 103 |
+
download_image_btn = gr.Button("画像をダウンロード", variant="secondary")
|
| 104 |
+
|
| 105 |
+
# JSONデータ表示
|
| 106 |
+
output_json = gr.JSON(
|
| 107 |
+
label="ポーズデータ (JSON)",
|
| 108 |
+
elem_id="output_json"
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
# JSONダウンロードボタン
|
| 112 |
+
download_json_btn = gr.Button("JSONをダウンロード", variant="secondary")
|
| 113 |
+
```
|
| 114 |
+
|
| 115 |
+
### 5. スタイリング考慮事項
|
| 116 |
+
- キャンバスは固定表示サイズ640x640
|
| 117 |
+
- レスポンシブ対応は必須ではない(仕様書に記載なし)
|
| 118 |
+
- Hugging Face Spacesでの表示を考慮
|
| 119 |
+
|
| 120 |
+
## 参考
|
| 121 |
+
- `./refs/dwpose_modifier`のUIレイアウト
|
| 122 |
+
- 特にタブ1とタブ2のマージされた形を意識
|
| 123 |
+
|
| 124 |
+
## 完了条件
|
| 125 |
+
- [ ] 3カラムレイアウトが正しく表示される
|
| 126 |
+
- [ ] すべての入力コンポーネントが配置されている
|
| 127 |
+
- [ ] Canvasエリアが正しく表示される(640x640)
|
| 128 |
+
- [ ] 出力部のコンポーネントが配置されている
|
| 129 |
+
- [ ] elem_idが適切に設定されている(JavaScript連携用)
|
issues/003_DWPoseモデル統合.md
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Issue: DWPoseモデル統合
|
| 2 |
+
|
| 3 |
+
## 概要
|
| 4 |
+
Hugging FaceからDWPoseモデルを取得し、画像からポーズを検出する機能を実装する。
|
| 5 |
+
|
| 6 |
+
## 背景
|
| 7 |
+
DWPoseは、OpenPoseと異なり、つま先、手、顔の詳細情報を含む高精度なポーズ推定モデル。`./refs/dwpose_modifier`の実装を参考にしつつ、シンプルな構造で統合する。
|
| 8 |
+
|
| 9 |
+
## 実装内容
|
| 10 |
+
|
| 11 |
+
### 1. 必要な依存関係の追加
|
| 12 |
+
requirements.txtに以下を追加:
|
| 13 |
+
```
|
| 14 |
+
huggingface-hub
|
| 15 |
+
onnxruntime
|
| 16 |
+
torch # CUDA検出用
|
| 17 |
+
```
|
| 18 |
+
|
| 19 |
+
### 2. DWPoseマネージャーの作成
|
| 20 |
+
`utils/dwpose_manager.py`:
|
| 21 |
+
```python
|
| 22 |
+
import os
|
| 23 |
+
from huggingface_hub import hf_hub_download
|
| 24 |
+
import onnxruntime as ort
|
| 25 |
+
|
| 26 |
+
class DWPoseManager:
|
| 27 |
+
def __init__(self):
|
| 28 |
+
self.model_repo = "yzd-v/DWPose"
|
| 29 |
+
self.cache_dir = "./models"
|
| 30 |
+
self.yolox_session = None
|
| 31 |
+
self.dwpose_session = None
|
| 32 |
+
|
| 33 |
+
def initialize(self):
|
| 34 |
+
"""モデルのダウンロードと初期化"""
|
| 35 |
+
try:
|
| 36 |
+
# YOLOXモデル
|
| 37 |
+
yolox_path = hf_hub_download(
|
| 38 |
+
repo_id=self.model_repo,
|
| 39 |
+
filename="yolox_l.onnx",
|
| 40 |
+
cache_dir=self.cache_dir
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
# DWPoseモデル
|
| 44 |
+
dwpose_path = hf_hub_download(
|
| 45 |
+
repo_id=self.model_repo,
|
| 46 |
+
filename="dw-ll_ucoco_384.onnx",
|
| 47 |
+
cache_dir=self.cache_dir
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
# ONNXセッション作成
|
| 51 |
+
providers = ['CUDAExecutionProvider', 'CPUExecutionProvider']
|
| 52 |
+
self.yolox_session = ort.InferenceSession(yolox_path, providers=providers)
|
| 53 |
+
self.dwpose_session = ort.InferenceSession(dwpose_path, providers=providers)
|
| 54 |
+
|
| 55 |
+
return True
|
| 56 |
+
except Exception as e:
|
| 57 |
+
return False, str(e)
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
### 3. DWPose検出器の実装
|
| 61 |
+
`utils/dwpose_detector.py`:
|
| 62 |
+
```python
|
| 63 |
+
import numpy as np
|
| 64 |
+
import cv2
|
| 65 |
+
|
| 66 |
+
class DWPoseDetector:
|
| 67 |
+
def __init__(self, manager):
|
| 68 |
+
self.manager = manager
|
| 69 |
+
self.input_size = 640 # YOLOX入力サイズ
|
| 70 |
+
self.pose_input_size = 384 # DWPose入力サイズ
|
| 71 |
+
|
| 72 |
+
def detect(self, image):
|
| 73 |
+
"""画像からポーズを検出"""
|
| 74 |
+
try:
|
| 75 |
+
# 1. 人物検出(YOLOX)
|
| 76 |
+
persons = self._detect_persons(image)
|
| 77 |
+
if len(persons) == 0:
|
| 78 |
+
return None, "人物が検出されませんでした"
|
| 79 |
+
|
| 80 |
+
# 2. ポーズ推定(DWPose)
|
| 81 |
+
poses = []
|
| 82 |
+
for person_box in persons:
|
| 83 |
+
pose = self._detect_pose(image, person_box)
|
| 84 |
+
if pose is not None:
|
| 85 |
+
poses.append(pose)
|
| 86 |
+
|
| 87 |
+
# 3. 最も大きい人物のポーズを返す
|
| 88 |
+
if len(poses) > 0:
|
| 89 |
+
return poses[0], None
|
| 90 |
+
else:
|
| 91 |
+
return None, "ポーズを検出できませんでした"
|
| 92 |
+
|
| 93 |
+
except Exception as e:
|
| 94 |
+
return None, f"エラーが発生しました: {str(e)}"
|
| 95 |
+
```
|
| 96 |
+
|
| 97 |
+
### 4. ポーズデータ形式
|
| 98 |
+
DWPoseの出力形式(フラット配列):
|
| 99 |
+
```python
|
| 100 |
+
# 各キーポイント: [x, y, confidence] × キーポイント数
|
| 101 |
+
# - body: 17キーポイント
|
| 102 |
+
# - foot: 6キーポイント(つま先含む)
|
| 103 |
+
# - face: 68キーポイント
|
| 104 |
+
# - hand: 左右各21キーポイント
|
| 105 |
+
pose_data = {
|
| 106 |
+
"bodies": {
|
| 107 |
+
"candidate": [[x1, y1], [x2, y2], ...], # 全キーポイントの座標
|
| 108 |
+
"subset": [[indices..., score, count]] # 人物ごとのキーポイントインデックス
|
| 109 |
+
},
|
| 110 |
+
"faces": [...], # 顔のキーポイント
|
| 111 |
+
"hands": [...], # 手のキーポイント
|
| 112 |
+
"resolution": [width, height]
|
| 113 |
+
}
|
| 114 |
+
```
|
| 115 |
+
|
| 116 |
+
### 5. エラーハンドリング
|
| 117 |
+
```python
|
| 118 |
+
def safe_detect(detector, image):
|
| 119 |
+
"""安全なポーズ検出(Gradio用)"""
|
| 120 |
+
try:
|
| 121 |
+
pose_data, error = detector.detect(image)
|
| 122 |
+
if error:
|
| 123 |
+
gr.Info(error) # Gradioトースト通知
|
| 124 |
+
return None
|
| 125 |
+
return pose_data
|
| 126 |
+
except Exception as e:
|
| 127 |
+
gr.Info(f"予期しないエラー: {str(e)}")
|
| 128 |
+
return None
|
| 129 |
+
```
|
| 130 |
+
|
| 131 |
+
## 参考
|
| 132 |
+
- `./refs/dwpose_modifier/core/dwpose/`の実装
|
| 133 |
+
- 特に`manager.py`、`detection/detector.py`を重点的に参考
|
| 134 |
+
- OpenPose形式への変換ロジック
|
| 135 |
+
|
| 136 |
+
## 完了条件
|
| 137 |
+
- [ ] Hugging Faceからモデルが自動ダウンロードされる
|
| 138 |
+
- [ ] 画像を入力してポーズが検出できる
|
| 139 |
+
- [ ] エラー時にGradioトーストで通知される
|
| 140 |
+
- [ ] つま先、手、顔の情報が含まれている
|
| 141 |
+
- [ ] dwpose_modifierと同じデータ形式で出力される
|
issues/004_Canvas要素初期化.md
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Issue: Canvas要素初期化
|
| 2 |
+
|
| 3 |
+
## 概要
|
| 4 |
+
GradioとCanvasの連携を確立し、JavaScriptによるCanvas操作の基盤を構築する。dwpose_modifierで苦労した初期化問題を回避する。
|
| 5 |
+
|
| 6 |
+
## 背景
|
| 7 |
+
Canvas初期化はdwpose_modifierで最も苦労した部分。Gradio要素の遅延生成に対応した堅牢な初期化処理が必要。
|
| 8 |
+
|
| 9 |
+
## 実装内容
|
| 10 |
+
|
| 11 |
+
### 1. JavaScript埋め込み構造
|
| 12 |
+
`static/pose_editor.js`:
|
| 13 |
+
```javascript
|
| 14 |
+
// グローバル変数
|
| 15 |
+
let canvas = null;
|
| 16 |
+
let ctx = null;
|
| 17 |
+
let poseData = null;
|
| 18 |
+
let isInitialized = false;
|
| 19 |
+
|
| 20 |
+
// デバッグログ関数
|
| 21 |
+
function debugLog(message) {
|
| 22 |
+
console.log(`[DWPose Editor] ${new Date().toISOString()} - ${message}`);
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
// Canvas初期化関数(複数の初期化タイミングに対応)
|
| 26 |
+
function initializeCanvas() {
|
| 27 |
+
debugLog("initializeCanvas called");
|
| 28 |
+
|
| 29 |
+
if (isInitialized) {
|
| 30 |
+
debugLog("Already initialized, skipping");
|
| 31 |
+
return;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
canvas = document.getElementById('pose_canvas');
|
| 35 |
+
if (!canvas) {
|
| 36 |
+
debugLog("Canvas not found, retrying...");
|
| 37 |
+
setTimeout(initializeCanvas, 100);
|
| 38 |
+
return;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
ctx = canvas.getContext('2d');
|
| 42 |
+
if (!ctx) {
|
| 43 |
+
debugLog("Failed to get 2d context");
|
| 44 |
+
return;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
// Canvas設定
|
| 48 |
+
canvas.width = 640;
|
| 49 |
+
canvas.height = 640;
|
| 50 |
+
|
| 51 |
+
// 初期描画
|
| 52 |
+
clearCanvas();
|
| 53 |
+
|
| 54 |
+
isInitialized = true;
|
| 55 |
+
debugLog("Canvas initialized successfully");
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
// 複数の初期化トリガー
|
| 59 |
+
document.addEventListener('DOMContentLoaded', initializeCanvas);
|
| 60 |
+
window.addEventListener('load', initializeCanvas);
|
| 61 |
+
|
| 62 |
+
// Gradio固有の初期化(MutationObserver使用)
|
| 63 |
+
const observer = new MutationObserver((mutations) => {
|
| 64 |
+
if (document.getElementById('pose_canvas') && !isInitialized) {
|
| 65 |
+
initializeCanvas();
|
| 66 |
+
}
|
| 67 |
+
});
|
| 68 |
+
|
| 69 |
+
// body要素の監視開始
|
| 70 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 71 |
+
observer.observe(document.body, {
|
| 72 |
+
childList: true,
|
| 73 |
+
subtree: true
|
| 74 |
+
});
|
| 75 |
+
});
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
+
### 2. Gradioとの連携
|
| 79 |
+
`app.py`での埋め込み:
|
| 80 |
+
```python
|
| 81 |
+
def load_javascript():
|
| 82 |
+
"""JavaScriptファイルを読み込む"""
|
| 83 |
+
js_path = os.path.join(os.path.dirname(__file__), "static", "pose_editor.js")
|
| 84 |
+
with open(js_path, "r", encoding="utf-8") as f:
|
| 85 |
+
return f"<script>{f.read()}</script>"
|
| 86 |
+
|
| 87 |
+
# Gradio Blocksでの使用
|
| 88 |
+
with gr.Blocks(head=load_javascript()) as demo:
|
| 89 |
+
# UI定義
|
| 90 |
+
pose_canvas = gr.HTML(
|
| 91 |
+
elem_id="pose_canvas_container",
|
| 92 |
+
value='<canvas id="pose_canvas" width="640" height="640" style="border: 1px solid #ccc; cursor: crosshair;"></canvas>'
|
| 93 |
+
)
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
### 3. Canvas基本操作関数
|
| 97 |
+
```javascript
|
| 98 |
+
// Canvas クリア
|
| 99 |
+
function clearCanvas() {
|
| 100 |
+
if (!ctx) return;
|
| 101 |
+
ctx.fillStyle = '#f0f0f0';
|
| 102 |
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
// エラー表示
|
| 106 |
+
function showCanvasError(message) {
|
| 107 |
+
if (!ctx) return;
|
| 108 |
+
clearCanvas();
|
| 109 |
+
ctx.fillStyle = '#ff0000';
|
| 110 |
+
ctx.font = '16px Arial';
|
| 111 |
+
ctx.textAlign = 'center';
|
| 112 |
+
ctx.fillText(message, canvas.width / 2, canvas.height / 2);
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
// Canvas状態チェック
|
| 116 |
+
function isCanvasReady() {
|
| 117 |
+
return canvas && ctx && isInitialized;
|
| 118 |
+
}
|
| 119 |
+
```
|
| 120 |
+
|
| 121 |
+
### 4. Gradioイベントとの連携準備
|
| 122 |
+
```javascript
|
| 123 |
+
// Gradioからのデータ受信用
|
| 124 |
+
window.updatePoseData = function(data) {
|
| 125 |
+
debugLog("updatePoseData called");
|
| 126 |
+
if (!isCanvasReady()) {
|
| 127 |
+
debugLog("Canvas not ready");
|
| 128 |
+
return;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
poseData = data;
|
| 132 |
+
// TODO: 描画処理を呼び出す
|
| 133 |
+
};
|
| 134 |
+
|
| 135 |
+
// Gradioへのデータ送信用
|
| 136 |
+
window.getPoseData = function() {
|
| 137 |
+
return poseData;
|
| 138 |
+
};
|
| 139 |
+
```
|
| 140 |
+
|
| 141 |
+
### 5. エラーハンドリング
|
| 142 |
+
```javascript
|
| 143 |
+
// グローバルエラーハンドラー
|
| 144 |
+
window.addEventListener('error', (e) => {
|
| 145 |
+
debugLog(`Global error: ${e.message}`);
|
| 146 |
+
if (isCanvasReady()) {
|
| 147 |
+
showCanvasError('エラーが発生しました');
|
| 148 |
+
}
|
| 149 |
+
});
|
| 150 |
+
|
| 151 |
+
// Canvas操作のtry-catch
|
| 152 |
+
function safeCanvasOperation(operation) {
|
| 153 |
+
try {
|
| 154 |
+
if (!isCanvasReady()) {
|
| 155 |
+
debugLog("Canvas not ready for operation");
|
| 156 |
+
return false;
|
| 157 |
+
}
|
| 158 |
+
operation();
|
| 159 |
+
return true;
|
| 160 |
+
} catch (e) {
|
| 161 |
+
debugLog(`Canvas operation error: ${e.message}`);
|
| 162 |
+
return false;
|
| 163 |
+
}
|
| 164 |
+
}
|
| 165 |
+
```
|
| 166 |
+
|
| 167 |
+
## 参考
|
| 168 |
+
- `./refs/dwpose_modifier/issues/019_test_gradio_simple実験継続_complete.md`
|
| 169 |
+
- `./refs/dwpose_modifier/issues/023_キャンバス描画重複問題修正_complete.md`
|
| 170 |
+
- 特に初期化タイミングとMutationObserverの使用法
|
| 171 |
+
|
| 172 |
+
## 完了条件
|
| 173 |
+
- [ ] ページロード時にCanvasが正しく初期化される
|
| 174 |
+
- [ ] Gradio要素の遅延生成に対応している
|
| 175 |
+
- [ ] デバッグログが適切に出力される
|
| 176 |
+
- [ ] エラー時に適切なメッセージが表示される
|
| 177 |
+
- [ ] Canvas操作の基本関数が実装されている
|
issues/005_ポーズ基本描画機能.md
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Issue: ポーズ基本描画機能
|
| 2 |
+
|
| 3 |
+
## 概要
|
| 4 |
+
DWPoseデータをCanvas上に描画する基本機能を実装する。dwpose_modifierの描画ロジックを参考に、スティックフィギュア形式で表示。
|
| 5 |
+
|
| 6 |
+
## 背景
|
| 7 |
+
Issue #004で初期化したCanvas上に、DWPoseのキーポイントと接続線を描画する。色分けもdwpose_modifierに準拠。
|
| 8 |
+
|
| 9 |
+
## 実装内容
|
| 10 |
+
|
| 11 |
+
### 1. DWPose接続定義
|
| 12 |
+
`static/pose_editor.js`に追加:
|
| 13 |
+
```javascript
|
| 14 |
+
// DWPoseの接続定義(dwpose_modifierと同じ)
|
| 15 |
+
const BODY_CONNECTIONS = [
|
| 16 |
+
[0, 1], [1, 2], [2, 3], [3, 4], // 右腕
|
| 17 |
+
[0, 5], [5, 6], [6, 7], [7, 8], // 左腕
|
| 18 |
+
[0, 9], [9, 10], [10, 11], // 右脚
|
| 19 |
+
[0, 12], [12, 13], [13, 14], // 左脚
|
| 20 |
+
[0, 15], [15, 16] // 首・頭
|
| 21 |
+
];
|
| 22 |
+
|
| 23 |
+
// 色定義(dwpose_modifierから)
|
| 24 |
+
const POSE_COLORS = {
|
| 25 |
+
body: '#ff0055',
|
| 26 |
+
hand: '#ff9500',
|
| 27 |
+
face: '#00ff00',
|
| 28 |
+
bodyLine: '#ff0055',
|
| 29 |
+
handLine: '#ff9500',
|
| 30 |
+
faceLine: '#00ff00'
|
| 31 |
+
};
|
| 32 |
+
|
| 33 |
+
// キーポイント半径
|
| 34 |
+
const KEYPOINT_RADIUS = 4;
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
### 2. 基本描画関数
|
| 38 |
+
```javascript
|
| 39 |
+
// ポーズ全体の描画
|
| 40 |
+
function drawPose(poseData, drawHands = true, drawFace = true) {
|
| 41 |
+
if (!isCanvasReady() || !poseData) return;
|
| 42 |
+
|
| 43 |
+
clearCanvas();
|
| 44 |
+
|
| 45 |
+
// 背景画像があれば描画(後で実装)
|
| 46 |
+
|
| 47 |
+
// ボディの描画
|
| 48 |
+
drawBody(poseData);
|
| 49 |
+
|
| 50 |
+
// 手の描画
|
| 51 |
+
if (drawHands && poseData.hands) {
|
| 52 |
+
drawHands(poseData.hands);
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
// 顔の描画
|
| 56 |
+
if (drawFace && poseData.faces) {
|
| 57 |
+
drawFace(poseData.faces);
|
| 58 |
+
}
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
// ボディ描画
|
| 62 |
+
function drawBody(poseData) {
|
| 63 |
+
if (!poseData.bodies || !poseData.bodies.candidate) return;
|
| 64 |
+
|
| 65 |
+
const candidates = poseData.bodies.candidate;
|
| 66 |
+
const subset = poseData.bodies.subset;
|
| 67 |
+
|
| 68 |
+
if (subset.length === 0) return;
|
| 69 |
+
|
| 70 |
+
// 最初の人物のみ描画(単一人物想定)
|
| 71 |
+
const person = subset[0];
|
| 72 |
+
|
| 73 |
+
// 接続線の描画
|
| 74 |
+
ctx.strokeStyle = POSE_COLORS.bodyLine;
|
| 75 |
+
ctx.lineWidth = 3;
|
| 76 |
+
|
| 77 |
+
for (const [start, end] of BODY_CONNECTIONS) {
|
| 78 |
+
const startIdx = person[start];
|
| 79 |
+
const endIdx = person[end];
|
| 80 |
+
|
| 81 |
+
if (startIdx >= 0 && endIdx >= 0) {
|
| 82 |
+
const startPoint = candidates[startIdx];
|
| 83 |
+
const endPoint = candidates[endIdx];
|
| 84 |
+
|
| 85 |
+
if (startPoint && endPoint) {
|
| 86 |
+
ctx.beginPath();
|
| 87 |
+
ctx.moveTo(startPoint[0], startPoint[1]);
|
| 88 |
+
ctx.lineTo(endPoint[0], endPoint[1]);
|
| 89 |
+
ctx.stroke();
|
| 90 |
+
}
|
| 91 |
+
}
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
// キーポイントの描画
|
| 95 |
+
ctx.fillStyle = POSE_COLORS.body;
|
| 96 |
+
for (let i = 0; i < 17; i++) { // DWPoseボディは17キーポイント
|
| 97 |
+
const idx = person[i];
|
| 98 |
+
if (idx >= 0) {
|
| 99 |
+
const point = candidates[idx];
|
| 100 |
+
if (point) {
|
| 101 |
+
drawKeypoint(point[0], point[1]);
|
| 102 |
+
}
|
| 103 |
+
}
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
// キーポイント描画
|
| 108 |
+
function drawKeypoint(x, y, radius = KEYPOINT_RADIUS) {
|
| 109 |
+
ctx.beginPath();
|
| 110 |
+
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
| 111 |
+
ctx.fill();
|
| 112 |
+
}
|
| 113 |
+
```
|
| 114 |
+
|
| 115 |
+
### 3. 手と顔の描画(簡易版)
|
| 116 |
+
```javascript
|
| 117 |
+
// 手の描画(21キーポイント × 2)
|
| 118 |
+
function drawHands(handsData) {
|
| 119 |
+
if (!handsData || handsData.length === 0) return;
|
| 120 |
+
|
| 121 |
+
ctx.fillStyle = POSE_COLORS.hand;
|
| 122 |
+
ctx.strokeStyle = POSE_COLORS.handLine;
|
| 123 |
+
ctx.lineWidth = 2;
|
| 124 |
+
|
| 125 |
+
// 左右の手を描画
|
| 126 |
+
handsData.forEach((hand) => {
|
| 127 |
+
if (hand && hand.length > 0) {
|
| 128 |
+
// TODO: 手の接続線定義を追加
|
| 129 |
+
// とりあえずキーポイントのみ描画
|
| 130 |
+
for (let i = 0; i < hand.length; i += 3) {
|
| 131 |
+
const x = hand[i];
|
| 132 |
+
const y = hand[i + 1];
|
| 133 |
+
const conf = hand[i + 2];
|
| 134 |
+
|
| 135 |
+
if (conf > 0.3) { // 信頼度閾値
|
| 136 |
+
drawKeypoint(x, y, 3);
|
| 137 |
+
}
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
});
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
// 顔の描画(68キーポイント)
|
| 144 |
+
function drawFace(facesData) {
|
| 145 |
+
if (!facesData || facesData.length === 0) return;
|
| 146 |
+
|
| 147 |
+
ctx.fillStyle = POSE_COLORS.face;
|
| 148 |
+
|
| 149 |
+
const face = facesData[0]; // 最初の顔のみ
|
| 150 |
+
if (face && face.length > 0) {
|
| 151 |
+
for (let i = 0; i < face.length; i += 3) {
|
| 152 |
+
const x = face[i];
|
| 153 |
+
const y = face[i + 1];
|
| 154 |
+
const conf = face[i + 2];
|
| 155 |
+
|
| 156 |
+
if (conf > 0.3) {
|
| 157 |
+
drawKeypoint(x, y, 2);
|
| 158 |
+
}
|
| 159 |
+
}
|
| 160 |
+
}
|
| 161 |
+
}
|
| 162 |
+
```
|
| 163 |
+
|
| 164 |
+
### 4. 座標変換対応
|
| 165 |
+
```javascript
|
| 166 |
+
// データ解像度とCanvas表示サイズの変換
|
| 167 |
+
function transformCoordinate(x, y, dataWidth, dataHeight) {
|
| 168 |
+
const scaleX = canvas.width / dataWidth;
|
| 169 |
+
const scaleY = canvas.height / dataHeight;
|
| 170 |
+
|
| 171 |
+
return {
|
| 172 |
+
x: x * scaleX,
|
| 173 |
+
y: y * scaleY
|
| 174 |
+
};
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
// 描画時に座標変換を適用
|
| 178 |
+
function drawKeypointScaled(x, y, dataRes, radius = KEYPOINT_RADIUS) {
|
| 179 |
+
const scaled = transformCoordinate(x, y, dataRes[0], dataRes[1]);
|
| 180 |
+
drawKeypoint(scaled.x, scaled.y, radius);
|
| 181 |
+
}
|
| 182 |
+
```
|
| 183 |
+
|
| 184 |
+
### 5. Gradioからの呼び出し
|
| 185 |
+
```javascript
|
| 186 |
+
// Gradioからポーズデータを受け取って描画
|
| 187 |
+
window.updatePoseData = function(data, drawHands, drawFace) {
|
| 188 |
+
debugLog("updatePoseData called");
|
| 189 |
+
if (!isCanvasReady()) {
|
| 190 |
+
debugLog("Canvas not ready");
|
| 191 |
+
return;
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
poseData = data;
|
| 195 |
+
drawPose(poseData, drawHands, drawFace);
|
| 196 |
+
};
|
| 197 |
+
```
|
| 198 |
+
|
| 199 |
+
## 参考
|
| 200 |
+
- `./refs/dwpose_modifier`の描画ロジック
|
| 201 |
+
- 特に色定義、接続定義、座標変換処理
|
| 202 |
+
|
| 203 |
+
## 完了条件
|
| 204 |
+
- [ ] DWPoseのボディが正しく描画される
|
| 205 |
+
- [ ] 手と顔のキーポイントが描画される
|
| 206 |
+
- [ ] 色分けがdwpose_modifierと同じ
|
| 207 |
+
- [ ] 座標変換が正しく機能する
|
| 208 |
+
- [ ] チェックボックスに応じて手顔の表示/非表示が切り替わる
|
issues/006_画像アップロード処理.md
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Issue: 画像アップロード処理
|
| 2 |
+
|
| 3 |
+
## 概要
|
| 4 |
+
参考画像のアップロード機能を実装。画像はCanvasの背景として表示し、長辺を表示サイズに合わせてリサイズする。
|
| 5 |
+
|
| 6 |
+
## 背景
|
| 7 |
+
ユーザーがアップロードした画像を背景として表示し、その上にポーズを編集できるようにする。
|
| 8 |
+
|
| 9 |
+
## 実装内容
|
| 10 |
+
|
| 11 |
+
### 1. 画像処理ユーティリティ
|
| 12 |
+
`utils/image_utils.py`:
|
| 13 |
+
```python
|
| 14 |
+
from PIL import Image
|
| 15 |
+
import numpy as np
|
| 16 |
+
|
| 17 |
+
def resize_image_aspect_ratio(image, target_size=640):
|
| 18 |
+
"""
|
| 19 |
+
画像の長辺をtarget_sizeに合わせてアスペクト比を保持したままリサイズ
|
| 20 |
+
"""
|
| 21 |
+
width, height = image.size
|
| 22 |
+
|
| 23 |
+
# 長辺を特定
|
| 24 |
+
if width > height:
|
| 25 |
+
# 横長
|
| 26 |
+
new_width = target_size
|
| 27 |
+
new_height = int(height * (target_size / width))
|
| 28 |
+
else:
|
| 29 |
+
# 縦長または正方形
|
| 30 |
+
new_height = target_size
|
| 31 |
+
new_width = int(width * (target_size / height))
|
| 32 |
+
|
| 33 |
+
# リサイズ
|
| 34 |
+
resized = image.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
| 35 |
+
|
| 36 |
+
# Canvasサイズに合わせて中央配置用の情報を返す
|
| 37 |
+
offset_x = (target_size - new_width) // 2
|
| 38 |
+
offset_y = (target_size - new_height) // 2
|
| 39 |
+
|
| 40 |
+
return resized, offset_x, offset_y
|
| 41 |
+
|
| 42 |
+
def image_to_base64(image):
|
| 43 |
+
"""PIL ImageをBase64エンコード"""
|
| 44 |
+
import io
|
| 45 |
+
import base64
|
| 46 |
+
|
| 47 |
+
buffer = io.BytesIO()
|
| 48 |
+
image.save(buffer, format="PNG")
|
| 49 |
+
img_str = base64.b64encode(buffer.getvalue()).decode()
|
| 50 |
+
return f"data:image/png;base64,{img_str}"
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
### 2. Gradio画像アップロードハンドラ
|
| 54 |
+
`app.py`に追加:
|
| 55 |
+
```python
|
| 56 |
+
def handle_image_upload(image):
|
| 57 |
+
"""
|
| 58 |
+
画像アップロード時の処理
|
| 59 |
+
"""
|
| 60 |
+
if image is None:
|
| 61 |
+
return None, None, gr.update()
|
| 62 |
+
|
| 63 |
+
try:
|
| 64 |
+
# 画像をリサイズ
|
| 65 |
+
resized_img, offset_x, offset_y = resize_image_aspect_ratio(image)
|
| 66 |
+
|
| 67 |
+
# Base64エンコード(JavaScript用)
|
| 68 |
+
img_base64 = image_to_base64(resized_img)
|
| 69 |
+
|
| 70 |
+
# 画像情報を保存
|
| 71 |
+
image_info = {
|
| 72 |
+
"base64": img_base64,
|
| 73 |
+
"width": resized_img.width,
|
| 74 |
+
"height": resized_img.height,
|
| 75 |
+
"offset_x": offset_x,
|
| 76 |
+
"offset_y": offset_y,
|
| 77 |
+
"original_size": image.size
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
# JavaScriptに画像を送信
|
| 81 |
+
update_js = f"""
|
| 82 |
+
<script>
|
| 83 |
+
setTimeout(() => {{
|
| 84 |
+
if (window.updateBackgroundImage) {{
|
| 85 |
+
window.updateBackgroundImage({json.dumps(image_info)});
|
| 86 |
+
}}
|
| 87 |
+
}}, 100);
|
| 88 |
+
</script>
|
| 89 |
+
"""
|
| 90 |
+
|
| 91 |
+
return image_info, update_js, gr.Info("画像をアップロードしました")
|
| 92 |
+
|
| 93 |
+
except Exception as e:
|
| 94 |
+
return None, None, gr.Error(f"画像処理エラー: {str(e)}")
|
| 95 |
+
|
| 96 |
+
# UIコンポーネントとの接続
|
| 97 |
+
input_image.upload(
|
| 98 |
+
handle_image_upload,
|
| 99 |
+
inputs=[input_image],
|
| 100 |
+
outputs=[image_data, js_executor, None]
|
| 101 |
+
)
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
### 3. JavaScript側の背景画像表示
|
| 105 |
+
`static/pose_editor.js`に追加:
|
| 106 |
+
```javascript
|
| 107 |
+
// 背景画像管理
|
| 108 |
+
let backgroundImage = null;
|
| 109 |
+
let backgroundImageInfo = null;
|
| 110 |
+
|
| 111 |
+
// 背景画像の更新
|
| 112 |
+
window.updateBackgroundImage = function(imageInfo) {
|
| 113 |
+
debugLog("updateBackgroundImage called");
|
| 114 |
+
|
| 115 |
+
if (!imageInfo || !imageInfo.base64) {
|
| 116 |
+
backgroundImage = null;
|
| 117 |
+
backgroundImageInfo = null;
|
| 118 |
+
redrawCanvas();
|
| 119 |
+
return;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
// 新しい画像オブジェクトを作成
|
| 123 |
+
const img = new Image();
|
| 124 |
+
img.onload = function() {
|
| 125 |
+
backgroundImage = img;
|
| 126 |
+
backgroundImageInfo = imageInfo;
|
| 127 |
+
debugLog(`Background image loaded: ${imageInfo.width}x${imageInfo.height}`);
|
| 128 |
+
redrawCanvas();
|
| 129 |
+
};
|
| 130 |
+
|
| 131 |
+
img.onerror = function() {
|
| 132 |
+
debugLog("Failed to load background image");
|
| 133 |
+
backgroundImage = null;
|
| 134 |
+
backgroundImageInfo = null;
|
| 135 |
+
};
|
| 136 |
+
|
| 137 |
+
img.src = imageInfo.base64;
|
| 138 |
+
};
|
| 139 |
+
|
| 140 |
+
// 背景画像の描画
|
| 141 |
+
function drawBackgroundImage() {
|
| 142 |
+
if (!backgroundImage || !backgroundImageInfo) return;
|
| 143 |
+
|
| 144 |
+
ctx.save();
|
| 145 |
+
|
| 146 |
+
// 画像を中央に配置
|
| 147 |
+
ctx.drawImage(
|
| 148 |
+
backgroundImage,
|
| 149 |
+
backgroundImageInfo.offset_x,
|
| 150 |
+
backgroundImageInfo.offset_y,
|
| 151 |
+
backgroundImageInfo.width,
|
| 152 |
+
backgroundImageInfo.height
|
| 153 |
+
);
|
| 154 |
+
|
| 155 |
+
ctx.restore();
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
// Canvas再描画(背景込み)
|
| 159 |
+
function redrawCanvas() {
|
| 160 |
+
if (!isCanvasReady()) return;
|
| 161 |
+
|
| 162 |
+
clearCanvas();
|
| 163 |
+
drawBackgroundImage();
|
| 164 |
+
|
| 165 |
+
// ポーズがあれば描画
|
| 166 |
+
if (poseData) {
|
| 167 |
+
drawPose(poseData, currentDrawHands, currentDrawFace);
|
| 168 |
+
}
|
| 169 |
+
}
|
| 170 |
+
```
|
| 171 |
+
|
| 172 |
+
### 4. 画像座標とポーズ座標の整合性
|
| 173 |
+
```javascript
|
| 174 |
+
// 画像上の座標をCanvas座標に変換
|
| 175 |
+
function imageToCanvasCoord(x, y) {
|
| 176 |
+
if (!backgroundImageInfo) return {x, y};
|
| 177 |
+
|
| 178 |
+
// 画像のオフセットを考慮
|
| 179 |
+
return {
|
| 180 |
+
x: x + backgroundImageInfo.offset_x,
|
| 181 |
+
y: y + backgroundImageInfo.offset_y
|
| 182 |
+
};
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
// Canvas座標を画像座標に変換
|
| 186 |
+
function canvasToImageCoord(x, y) {
|
| 187 |
+
if (!backgroundImageInfo) return {x, y};
|
| 188 |
+
|
| 189 |
+
return {
|
| 190 |
+
x: x - backgroundImageInfo.offset_x,
|
| 191 |
+
y: y - backgroundImageInfo.offset_y
|
| 192 |
+
};
|
| 193 |
+
}
|
| 194 |
+
```
|
| 195 |
+
|
| 196 |
+
### 5. 画像なしでのDWPose失敗時の処理
|
| 197 |
+
```javascript
|
| 198 |
+
// DWPose検出失敗時も画像は表示
|
| 199 |
+
window.handleDWPoseError = function(imageInfo) {
|
| 200 |
+
debugLog("DWPose detection failed, showing image only");
|
| 201 |
+
|
| 202 |
+
// 画像だけ表示
|
| 203 |
+
updateBackgroundImage(imageInfo);
|
| 204 |
+
|
| 205 |
+
// ポーズデータをクリア
|
| 206 |
+
poseData = null;
|
| 207 |
+
};
|
| 208 |
+
```
|
| 209 |
+
|
| 210 |
+
## 参考
|
| 211 |
+
- 画像リサイズとアスペクト比保持のロジック
|
| 212 |
+
- Base64エンコーディングによるJavaScript連携
|
| 213 |
+
|
| 214 |
+
## 完了条件
|
| 215 |
+
- [ ] 画像アップロード時に長辺が640pxにリサイズされる
|
| 216 |
+
- [ ] アスペクト比が保持される
|
| 217 |
+
- [ ] 画像がCanvasの中央に表示される
|
| 218 |
+
- [ ] DWPose失敗時も画像が表示される
|
| 219 |
+
- [ ] 画像座標とCanvas座標の変換が正しく機能する
|
issues/007_DWPose自動抽出機能.md
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Issue: DWPose自動抽出機能
|
| 2 |
+
|
| 3 |
+
## 概要
|
| 4 |
+
画像アップロード時に自動的にDWPoseでポーズを抽出し、Canvas上に表示する機能を実装。
|
| 5 |
+
|
| 6 |
+
## 背景
|
| 7 |
+
Issue #003で統合したDWPoseモデルと、Issue #006の画像アップロード処理を連携させる。
|
| 8 |
+
|
| 9 |
+
## 実装内容
|
| 10 |
+
|
| 11 |
+
### 1. DWPose抽出処理の実装
|
| 12 |
+
`app.py`に追加:
|
| 13 |
+
```python
|
| 14 |
+
# グローバル変数としてDWPoseマネージャーを保持
|
| 15 |
+
dwpose_manager = None
|
| 16 |
+
dwpose_detector = None
|
| 17 |
+
|
| 18 |
+
def initialize_dwpose():
|
| 19 |
+
"""DWPoseの初期化"""
|
| 20 |
+
global dwpose_manager, dwpose_detector
|
| 21 |
+
|
| 22 |
+
try:
|
| 23 |
+
dwpose_manager = DWPoseManager()
|
| 24 |
+
success = dwpose_manager.initialize()
|
| 25 |
+
|
| 26 |
+
if success:
|
| 27 |
+
dwpose_detector = DWPoseDetector(dwpose_manager)
|
| 28 |
+
return True, "DWPoseモデルを初期化しました"
|
| 29 |
+
else:
|
| 30 |
+
return False, "DWPoseモデルの初期化に失敗しました"
|
| 31 |
+
except Exception as e:
|
| 32 |
+
return False, f"初期化エラー: {str(e)}"
|
| 33 |
+
|
| 34 |
+
def extract_pose_from_image(image, image_info):
|
| 35 |
+
"""
|
| 36 |
+
画像からポーズを抽出
|
| 37 |
+
"""
|
| 38 |
+
if image is None or dwpose_detector is None:
|
| 39 |
+
return None, "画像またはDWPoseが利用できません"
|
| 40 |
+
|
| 41 |
+
try:
|
| 42 |
+
# numpy配列に変換
|
| 43 |
+
image_np = np.array(image)
|
| 44 |
+
|
| 45 |
+
# DWPoseでポーズ検出
|
| 46 |
+
pose_data, error = dwpose_detector.detect(image_np)
|
| 47 |
+
|
| 48 |
+
if error:
|
| 49 |
+
return None, error
|
| 50 |
+
|
| 51 |
+
if pose_data is None:
|
| 52 |
+
return None, "ポーズが検出されませんでした"
|
| 53 |
+
|
| 54 |
+
# 解像度情報を追加
|
| 55 |
+
pose_data["resolution"] = [image.width, image.height]
|
| 56 |
+
|
| 57 |
+
return pose_data, None
|
| 58 |
+
|
| 59 |
+
except Exception as e:
|
| 60 |
+
return None, f"ポーズ抽出エラー: {str(e)}"
|
| 61 |
+
```
|
| 62 |
+
|
| 63 |
+
### 2. 画像アップロードとDWPose連携
|
| 64 |
+
```python
|
| 65 |
+
def handle_image_upload_with_dwpose(image):
|
| 66 |
+
"""
|
| 67 |
+
画像アップロード時の処理(DWPose自動実行)
|
| 68 |
+
"""
|
| 69 |
+
if image is None:
|
| 70 |
+
return None, None, None, gr.update()
|
| 71 |
+
|
| 72 |
+
try:
|
| 73 |
+
# 画像をリサイズ
|
| 74 |
+
resized_img, offset_x, offset_y = resize_image_aspect_ratio(image)
|
| 75 |
+
|
| 76 |
+
# Base64エンコード
|
| 77 |
+
img_base64 = image_to_base64(resized_img)
|
| 78 |
+
|
| 79 |
+
image_info = {
|
| 80 |
+
"base64": img_base64,
|
| 81 |
+
"width": resized_img.width,
|
| 82 |
+
"height": resized_img.height,
|
| 83 |
+
"offset_x": offset_x,
|
| 84 |
+
"offset_y": offset_y,
|
| 85 |
+
"original_size": image.size
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
# DWPoseでポーズ抽出
|
| 89 |
+
gr.Info("ポーズを検出中...")
|
| 90 |
+
pose_data, error = extract_pose_from_image(image, image_info)
|
| 91 |
+
|
| 92 |
+
if error:
|
| 93 |
+
# エラーでも画像は表示
|
| 94 |
+
update_js = f"""
|
| 95 |
+
<script>
|
| 96 |
+
setTimeout(() => {{
|
| 97 |
+
if (window.handleDWPoseError) {{
|
| 98 |
+
window.handleDWPoseError({json.dumps(image_info)});
|
| 99 |
+
}}
|
| 100 |
+
}}, 100);
|
| 101 |
+
</script>
|
| 102 |
+
"""
|
| 103 |
+
gr.Warning(error)
|
| 104 |
+
return image_info, None, update_js, gr.update()
|
| 105 |
+
|
| 106 |
+
# 成功時:画像とポーズ両方を更新
|
| 107 |
+
update_js = f"""
|
| 108 |
+
<script>
|
| 109 |
+
setTimeout(() => {{
|
| 110 |
+
if (window.updateBackgroundImage) {{
|
| 111 |
+
window.updateBackgroundImage({json.dumps(image_info)});
|
| 112 |
+
}}
|
| 113 |
+
if (window.updatePoseData) {{
|
| 114 |
+
window.updatePoseData({json.dumps(pose_data)}, true, true);
|
| 115 |
+
}}
|
| 116 |
+
}}, 100);
|
| 117 |
+
</script>
|
| 118 |
+
"""
|
| 119 |
+
|
| 120 |
+
gr.Info("ポーズを検出しました")
|
| 121 |
+
return image_info, pose_data, update_js, gr.update(value=pose_data)
|
| 122 |
+
|
| 123 |
+
except Exception as e:
|
| 124 |
+
return None, None, None, gr.Error(f"処理エラー: {str(e)}")
|
| 125 |
+
```
|
| 126 |
+
|
| 127 |
+
### 3. ポーズ座標の正規化チェック
|
| 128 |
+
`utils/pose_utils.py`:
|
| 129 |
+
```python
|
| 130 |
+
def normalize_pose_coordinates(pose_data, image_width, image_height):
|
| 131 |
+
"""
|
| 132 |
+
ポーズ座標が正規化されているかチェックし、必要なら変換
|
| 133 |
+
"""
|
| 134 |
+
if not pose_data or "bodies" not in pose_data:
|
| 135 |
+
return pose_data
|
| 136 |
+
|
| 137 |
+
candidates = pose_data["bodies"].get("candidate", [])
|
| 138 |
+
if not candidates:
|
| 139 |
+
return pose_data
|
| 140 |
+
|
| 141 |
+
# 最初のキーポイントで判定
|
| 142 |
+
first_point = candidates[0]
|
| 143 |
+
if len(first_point) >= 2:
|
| 144 |
+
x, y = first_point[0], first_point[1]
|
| 145 |
+
|
| 146 |
+
# 0-1の範囲なら正規化されている
|
| 147 |
+
if 0 <= x <= 1 and 0 <= y <= 1:
|
| 148 |
+
# ピクセル座標に変換
|
| 149 |
+
for i, point in enumerate(candidates):
|
| 150 |
+
if len(point) >= 2:
|
| 151 |
+
candidates[i][0] = point[0] * image_width
|
| 152 |
+
candidates[i][1] = point[1] * image_height
|
| 153 |
+
|
| 154 |
+
return pose_data
|
| 155 |
+
```
|
| 156 |
+
|
| 157 |
+
### 4. エラーリカバリー機能
|
| 158 |
+
```python
|
| 159 |
+
def retry_dwpose_detection(image):
|
| 160 |
+
"""
|
| 161 |
+
DWPose検出のリトライ(パラメータ調整)
|
| 162 |
+
"""
|
| 163 |
+
# 検出閾値を下げてリトライ
|
| 164 |
+
original_threshold = dwpose_detector.detection_threshold
|
| 165 |
+
dwpose_detector.detection_threshold = 0.3 # より低い閾値
|
| 166 |
+
|
| 167 |
+
try:
|
| 168 |
+
pose_data, error = extract_pose_from_image(image, None)
|
| 169 |
+
return pose_data, error
|
| 170 |
+
finally:
|
| 171 |
+
# 閾値を元に戻す
|
| 172 |
+
dwpose_detector.detection_threshold = original_threshold
|
| 173 |
+
```
|
| 174 |
+
|
| 175 |
+
### 5. デバッグ情報の出力
|
| 176 |
+
```python
|
| 177 |
+
def debug_pose_data(pose_data):
|
| 178 |
+
"""ポーズデータのデバッグ情報を出力"""
|
| 179 |
+
if not pose_data:
|
| 180 |
+
print("No pose data")
|
| 181 |
+
return
|
| 182 |
+
|
| 183 |
+
if "bodies" in pose_data:
|
| 184 |
+
bodies = pose_data["bodies"]
|
| 185 |
+
print(f"Candidates: {len(bodies.get('candidate', []))}")
|
| 186 |
+
print(f"Subsets: {len(bodies.get('subset', []))}")
|
| 187 |
+
|
| 188 |
+
if bodies.get('subset'):
|
| 189 |
+
person = bodies['subset'][0]
|
| 190 |
+
detected_keypoints = sum(1 for idx in person[:17] if idx >= 0)
|
| 191 |
+
print(f"Detected keypoints: {detected_keypoints}/17")
|
| 192 |
+
|
| 193 |
+
if "faces" in pose_data:
|
| 194 |
+
print(f"Face keypoints: {len(pose_data['faces'][0]) // 3 if pose_data['faces'] else 0}")
|
| 195 |
+
|
| 196 |
+
if "hands" in pose_data:
|
| 197 |
+
print(f"Hands detected: {len(pose_data['hands'])}")
|
| 198 |
+
```
|
| 199 |
+
|
| 200 |
+
## 参考
|
| 201 |
+
- `./refs/dwpose_modifier`のDWPose検出フロー
|
| 202 |
+
- エラーハンドリングとリトライロジック
|
| 203 |
+
|
| 204 |
+
## 完了条件
|
| 205 |
+
- [ ] 画像アップロード時に自動でDWPoseが実行される
|
| 206 |
+
- [ ] ポーズが検出されたらCanvas上に表示される
|
| 207 |
+
- [ ] エラー時はGradioトーストで通知される
|
| 208 |
+
- [ ] エラー時も画像は表示される
|
| 209 |
+
- [ ] 座標の正規化チェックが機能する
|
issues/008_座標変換システム.md
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Issue: 座標変換システム
|
| 2 |
+
|
| 3 |
+
## 概要
|
| 4 |
+
データ解像度とCanvas表示サイズの違いを吸収する座標変換システムを実装。dwpose_modifierで苦労した座標系の問題を解決。
|
| 5 |
+
|
| 6 |
+
## 背景
|
| 7 |
+
- データ解像度:512x512(可変)
|
| 8 |
+
- Canvas表示サイズ:640x640(固定)
|
| 9 |
+
- 画像サイズ:任意(アスペクト比保持)
|
| 10 |
+
|
| 11 |
+
これらの座標系を統一的に扱うシステムが必要。
|
| 12 |
+
|
| 13 |
+
## 実装内容
|
| 14 |
+
|
| 15 |
+
### 1. 座標変換クラス
|
| 16 |
+
`utils/coordinate_system.py`:
|
| 17 |
+
```python
|
| 18 |
+
class CoordinateTransformer:
|
| 19 |
+
def __init__(self):
|
| 20 |
+
self.data_width = 512
|
| 21 |
+
self.data_height = 512
|
| 22 |
+
self.canvas_width = 640
|
| 23 |
+
self.canvas_height = 640
|
| 24 |
+
self.image_width = None
|
| 25 |
+
self.image_height = None
|
| 26 |
+
self.image_offset_x = 0
|
| 27 |
+
self.image_offset_y = 0
|
| 28 |
+
|
| 29 |
+
def set_data_resolution(self, width, height):
|
| 30 |
+
"""データ解像度の設定"""
|
| 31 |
+
self.data_width = width
|
| 32 |
+
self.data_height = height
|
| 33 |
+
|
| 34 |
+
def set_image_info(self, width, height, offset_x, offset_y):
|
| 35 |
+
"""画像情報の設定"""
|
| 36 |
+
self.image_width = width
|
| 37 |
+
self.image_height = height
|
| 38 |
+
self.image_offset_x = offset_x
|
| 39 |
+
self.image_offset_y = offset_y
|
| 40 |
+
|
| 41 |
+
def data_to_canvas(self, x, y):
|
| 42 |
+
"""データ座標をCanvas座標に変換"""
|
| 43 |
+
scale_x = self.canvas_width / self.data_width
|
| 44 |
+
scale_y = self.canvas_height / self.data_height
|
| 45 |
+
|
| 46 |
+
return {
|
| 47 |
+
'x': x * scale_x,
|
| 48 |
+
'y': y * scale_y
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
def canvas_to_data(self, x, y):
|
| 52 |
+
"""Canvas座標をデータ座標に変換"""
|
| 53 |
+
scale_x = self.data_width / self.canvas_width
|
| 54 |
+
scale_y = self.data_height / self.canvas_height
|
| 55 |
+
|
| 56 |
+
return {
|
| 57 |
+
'x': x * scale_x,
|
| 58 |
+
'y': y * scale_y
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
def image_to_canvas(self, x, y):
|
| 62 |
+
"""画像座標をCanvas座標に変換"""
|
| 63 |
+
return {
|
| 64 |
+
'x': x + self.image_offset_x,
|
| 65 |
+
'y': y + self.image_offset_y
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
def canvas_to_image(self, x, y):
|
| 69 |
+
"""Canvas座標を画像座標に変換"""
|
| 70 |
+
return {
|
| 71 |
+
'x': x - self.image_offset_x,
|
| 72 |
+
'y': y - self.image_offset_y
|
| 73 |
+
}
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
### 2. JavaScript座標変換
|
| 77 |
+
`static/pose_editor.js`に追加:
|
| 78 |
+
```javascript
|
| 79 |
+
// 座標変換管理
|
| 80 |
+
class CoordinateManager {
|
| 81 |
+
constructor() {
|
| 82 |
+
this.dataWidth = 512;
|
| 83 |
+
this.dataHeight = 512;
|
| 84 |
+
this.canvasWidth = 640;
|
| 85 |
+
this.canvasHeight = 640;
|
| 86 |
+
this.imageInfo = null;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
setDataResolution(width, height) {
|
| 90 |
+
this.dataWidth = width;
|
| 91 |
+
this.dataHeight = height;
|
| 92 |
+
debugLog(`Data resolution set to ${width}x${height}`);
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
setImageInfo(imageInfo) {
|
| 96 |
+
this.imageInfo = imageInfo;
|
| 97 |
+
debugLog(`Image info set: ${imageInfo.width}x${imageInfo.height} offset(${imageInfo.offset_x}, ${imageInfo.offset_y})`);
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
// データ座標 → Canvas座標
|
| 101 |
+
dataToCanvas(x, y) {
|
| 102 |
+
const scaleX = this.canvasWidth / this.dataWidth;
|
| 103 |
+
const scaleY = this.canvasHeight / this.dataHeight;
|
| 104 |
+
|
| 105 |
+
return {
|
| 106 |
+
x: x * scaleX,
|
| 107 |
+
y: y * scaleY
|
| 108 |
+
};
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
// Canvas座標 → データ座標
|
| 112 |
+
canvasToData(x, y) {
|
| 113 |
+
const scaleX = this.dataWidth / this.canvasWidth;
|
| 114 |
+
const scaleY = this.dataHeight / this.canvasHeight;
|
| 115 |
+
|
| 116 |
+
return {
|
| 117 |
+
x: x * scaleX,
|
| 118 |
+
y: y * scaleY
|
| 119 |
+
};
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
// 画像座標 → Canvas座標
|
| 123 |
+
imageToCanvas(x, y) {
|
| 124 |
+
if (!this.imageInfo) return {x, y};
|
| 125 |
+
|
| 126 |
+
return {
|
| 127 |
+
x: x + this.imageInfo.offset_x,
|
| 128 |
+
y: y + this.imageInfo.offset_y
|
| 129 |
+
};
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
// Canvas座標 → 画像座標
|
| 133 |
+
canvasToImage(x, y) {
|
| 134 |
+
if (!this.imageInfo) return {x, y};
|
| 135 |
+
|
| 136 |
+
return {
|
| 137 |
+
x: x - this.imageInfo.offset_x,
|
| 138 |
+
y: y - this.imageInfo.offset_y
|
| 139 |
+
};
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
// 正規化座標検出と変換
|
| 143 |
+
normalizeIfNeeded(pose_data) {
|
| 144 |
+
if (!pose_data || !pose_data.bodies) return pose_data;
|
| 145 |
+
|
| 146 |
+
const candidates = pose_data.bodies.candidate;
|
| 147 |
+
if (!candidates || candidates.length === 0) return pose_data;
|
| 148 |
+
|
| 149 |
+
// 最初のキーポイントで正規化チェック
|
| 150 |
+
const firstPoint = candidates[0];
|
| 151 |
+
if (firstPoint && firstPoint.length >= 2) {
|
| 152 |
+
const x = firstPoint[0];
|
| 153 |
+
const y = firstPoint[1];
|
| 154 |
+
|
| 155 |
+
// 0-1の範囲なら正規化されている
|
| 156 |
+
if (x >= 0 && x <= 1 && y >= 0 && y <= 1) {
|
| 157 |
+
debugLog("Normalizing coordinates to pixel values");
|
| 158 |
+
|
| 159 |
+
// ピクセル座標に変換
|
| 160 |
+
for (let i = 0; i < candidates.length; i++) {
|
| 161 |
+
if (candidates[i] && candidates[i].length >= 2) {
|
| 162 |
+
candidates[i][0] *= this.dataWidth;
|
| 163 |
+
candidates[i][1] *= this.dataHeight;
|
| 164 |
+
}
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
// 手と顔も同様に処理
|
| 168 |
+
if (pose_data.hands) {
|
| 169 |
+
pose_data.hands.forEach(hand => {
|
| 170 |
+
if (hand) {
|
| 171 |
+
for (let i = 0; i < hand.length; i += 3) {
|
| 172 |
+
hand[i] *= this.dataWidth; // x
|
| 173 |
+
hand[i + 1] *= this.dataHeight; // y
|
| 174 |
+
}
|
| 175 |
+
}
|
| 176 |
+
});
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
if (pose_data.faces) {
|
| 180 |
+
pose_data.faces.forEach(face => {
|
| 181 |
+
if (face) {
|
| 182 |
+
for (let i = 0; i < face.length; i += 3) {
|
| 183 |
+
face[i] *= this.dataWidth; // x
|
| 184 |
+
face[i + 1] *= this.dataHeight; // y
|
| 185 |
+
}
|
| 186 |
+
}
|
| 187 |
+
});
|
| 188 |
+
}
|
| 189 |
+
}
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
return pose_data;
|
| 193 |
+
}
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
// グローバル座標マネージャー
|
| 197 |
+
const coordManager = new CoordinateManager();
|
| 198 |
+
```
|
| 199 |
+
|
| 200 |
+
### 3. ポーズ描画での座標変換適用
|
| 201 |
+
```javascript
|
| 202 |
+
// 座標変換を適用した描画関数
|
| 203 |
+
function drawKeypointTransformed(x, y, radius = KEYPOINT_RADIUS) {
|
| 204 |
+
const canvasCoord = coordManager.dataToCanvas(x, y);
|
| 205 |
+
drawKeypoint(canvasCoord.x, canvasCoord.y, radius);
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
function drawLineTransformed(x1, y1, x2, y2) {
|
| 209 |
+
const start = coordManager.dataToCanvas(x1, y1);
|
| 210 |
+
const end = coordManager.dataToCanvas(x2, y2);
|
| 211 |
+
|
| 212 |
+
ctx.beginPath();
|
| 213 |
+
ctx.moveTo(start.x, start.y);
|
| 214 |
+
ctx.lineTo(end.x, end.y);
|
| 215 |
+
ctx.stroke();
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
// ボディ描画を座標変換対応に更新
|
| 219 |
+
function drawBodyTransformed(poseData) {
|
| 220 |
+
if (!poseData.bodies || !poseData.bodies.candidate) return;
|
| 221 |
+
|
| 222 |
+
const candidates = poseData.bodies.candidate;
|
| 223 |
+
const subset = poseData.bodies.subset;
|
| 224 |
+
|
| 225 |
+
if (subset.length === 0) return;
|
| 226 |
+
|
| 227 |
+
const person = subset[0];
|
| 228 |
+
|
| 229 |
+
// 接続線の描画(座標変換適用)
|
| 230 |
+
ctx.strokeStyle = POSE_COLORS.bodyLine;
|
| 231 |
+
ctx.lineWidth = 3;
|
| 232 |
+
|
| 233 |
+
for (const [start, end] of BODY_CONNECTIONS) {
|
| 234 |
+
const startIdx = person[start];
|
| 235 |
+
const endIdx = person[end];
|
| 236 |
+
|
| 237 |
+
if (startIdx >= 0 && endIdx >= 0) {
|
| 238 |
+
const startPoint = candidates[startIdx];
|
| 239 |
+
const endPoint = candidates[endIdx];
|
| 240 |
+
|
| 241 |
+
if (startPoint && endPoint) {
|
| 242 |
+
drawLineTransformed(
|
| 243 |
+
startPoint[0], startPoint[1],
|
| 244 |
+
endPoint[0], endPoint[1]
|
| 245 |
+
);
|
| 246 |
+
}
|
| 247 |
+
}
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
// キーポイントの描画(座標変換適用)
|
| 251 |
+
ctx.fillStyle = POSE_COLORS.body;
|
| 252 |
+
for (let i = 0; i < 17; i++) {
|
| 253 |
+
const idx = person[i];
|
| 254 |
+
if (idx >= 0) {
|
| 255 |
+
const point = candidates[idx];
|
| 256 |
+
if (point) {
|
| 257 |
+
drawKeypointTransformed(point[0], point[1]);
|
| 258 |
+
}
|
| 259 |
+
}
|
| 260 |
+
}
|
| 261 |
+
}
|
| 262 |
+
```
|
| 263 |
+
|
| 264 |
+
### 4. マウス操作での座標変換
|
| 265 |
+
```javascript
|
| 266 |
+
// マウス座標をデータ座標に変換
|
| 267 |
+
function getDataCoordinateFromMouse(event) {
|
| 268 |
+
const rect = canvas.getBoundingClientRect();
|
| 269 |
+
const canvasX = event.clientX - rect.left;
|
| 270 |
+
const canvasY = event.clientY - rect.top;
|
| 271 |
+
|
| 272 |
+
return coordManager.canvasToData(canvasX, canvasY);
|
| 273 |
+
}
|
| 274 |
+
```
|
| 275 |
+
|
| 276 |
+
## 参考
|
| 277 |
+
- `./refs/dwpose_modifier`の座標変換ロジック
|
| 278 |
+
- 特に正規化座標の検出と変換処理
|
| 279 |
+
|
| 280 |
+
## 完了条件
|
| 281 |
+
- [ ] データ解像度とCanvas表示サイズの変換が正しく機能する
|
| 282 |
+
- [ ] 正規化座標の自動検出と変換ができる
|
| 283 |
+
- [ ] マウス操作時の座標変換が正確
|
| 284 |
+
- [ ] 画像オフセットを考慮した座標変換
|
| 285 |
+
- [ ] 手と顔の座標変換も対応している
|
issues/009_キーポイントドラッグ基本.md
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Issue: キーポイントドラッグ基本
|
| 2 |
+
|
| 3 |
+
## 概要
|
| 4 |
+
Canvas上のキーポイントをマウスでドラッグして編集する基本機能を実装。詳細モードでの個別キーポイント操作。
|
| 5 |
+
|
| 6 |
+
## 背景
|
| 7 |
+
Issue #008の座標変換システムを使って、キーポイントのドラッグ編集を実現する。dwpose_modifierで発生したドラッグ時の座標ジャンプ問題を回避。
|
| 8 |
+
|
| 9 |
+
## 実装内容
|
| 10 |
+
|
| 11 |
+
### 1. ドラッグ状態管理
|
| 12 |
+
`static/pose_editor.js`に追加:
|
| 13 |
+
```javascript
|
| 14 |
+
// ドラッグ状態管理
|
| 15 |
+
class DragManager {
|
| 16 |
+
constructor() {
|
| 17 |
+
this.isDragging = false;
|
| 18 |
+
this.dragTarget = null;
|
| 19 |
+
this.dragOffset = {x: 0, y: 0};
|
| 20 |
+
this.clickThreshold = 10; // ドラッグ判定閾値(ピクセル)
|
| 21 |
+
this.hoverTarget = null;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
startDrag(target, mouseX, mouseY) {
|
| 25 |
+
this.isDragging = true;
|
| 26 |
+
this.dragTarget = target;
|
| 27 |
+
|
| 28 |
+
// ドラッグオフセットを計算(キーポイント中心からのずれ)
|
| 29 |
+
const canvasCoord = coordManager.dataToCanvas(target.x, target.y);
|
| 30 |
+
this.dragOffset = {
|
| 31 |
+
x: mouseX - canvasCoord.x,
|
| 32 |
+
y: mouseY - canvasCoord.y
|
| 33 |
+
};
|
| 34 |
+
|
| 35 |
+
debugLog(`Drag started: ${target.type} ${target.index}`);
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
updateDrag(mouseX, mouseY) {
|
| 39 |
+
if (!this.isDragging || !this.dragTarget) return false;
|
| 40 |
+
|
| 41 |
+
// マウス位置からオフセットを引いて正確な位置を計算
|
| 42 |
+
const adjustedX = mouseX - this.dragOffset.x;
|
| 43 |
+
const adjustedY = mouseY - this.dragOffset.y;
|
| 44 |
+
|
| 45 |
+
// Canvas座標をデータ座標に変換
|
| 46 |
+
const dataCoord = coordManager.canvasToData(adjustedX, adjustedY);
|
| 47 |
+
|
| 48 |
+
// キーポイント位置を更新
|
| 49 |
+
this.updateKeypointPosition(this.dragTarget, dataCoord.x, dataCoord.y);
|
| 50 |
+
|
| 51 |
+
return true;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
endDrag() {
|
| 55 |
+
if (this.isDragging) {
|
| 56 |
+
debugLog(`Drag ended: ${this.dragTarget?.type} ${this.dragTarget?.index}`);
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
this.isDragging = false;
|
| 60 |
+
this.dragTarget = null;
|
| 61 |
+
this.dragOffset = {x: 0, y: 0};
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
updateKeypointPosition(target, x, y) {
|
| 65 |
+
if (!poseData) return;
|
| 66 |
+
|
| 67 |
+
if (target.type === 'body') {
|
| 68 |
+
const candidates = poseData.bodies.candidate;
|
| 69 |
+
const subset = poseData.bodies.subset[0];
|
| 70 |
+
const candidateIndex = subset[target.index];
|
| 71 |
+
|
| 72 |
+
if (candidateIndex >= 0 && candidates[candidateIndex]) {
|
| 73 |
+
candidates[candidateIndex][0] = x;
|
| 74 |
+
candidates[candidateIndex][1] = y;
|
| 75 |
+
}
|
| 76 |
+
} else if (target.type === 'hand') {
|
| 77 |
+
const hand = poseData.hands[target.handIndex];
|
| 78 |
+
if (hand) {
|
| 79 |
+
const pointIndex = target.index * 3;
|
| 80 |
+
hand[pointIndex] = x; // x座標
|
| 81 |
+
hand[pointIndex + 1] = y; // y座標
|
| 82 |
+
}
|
| 83 |
+
} else if (target.type === 'face') {
|
| 84 |
+
const face = poseData.faces[0];
|
| 85 |
+
if (face) {
|
| 86 |
+
const pointIndex = target.index * 3;
|
| 87 |
+
face[pointIndex] = x; // x座標
|
| 88 |
+
face[pointIndex + 1] = y; // y座標
|
| 89 |
+
}
|
| 90 |
+
}
|
| 91 |
+
}
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
const dragManager = new DragManager();
|
| 95 |
+
```
|
| 96 |
+
|
| 97 |
+
### 2. キーポイント検出
|
| 98 |
+
```javascript
|
| 99 |
+
// マウス位置にあるキーポイントを検出
|
| 100 |
+
function getKeypointAt(mouseX, mouseY) {
|
| 101 |
+
if (!poseData) return null;
|
| 102 |
+
|
| 103 |
+
const hitRadius = 12; // ヒット判定半径
|
| 104 |
+
|
| 105 |
+
// ボディキーポイントをチェック
|
| 106 |
+
if (poseData.bodies && poseData.bodies.candidate) {
|
| 107 |
+
const candidates = poseData.bodies.candidate;
|
| 108 |
+
const subset = poseData.bodies.subset[0];
|
| 109 |
+
|
| 110 |
+
for (let i = 0; i < 17; i++) {
|
| 111 |
+
const candidateIndex = subset[i];
|
| 112 |
+
if (candidateIndex >= 0) {
|
| 113 |
+
const point = candidates[candidateIndex];
|
| 114 |
+
if (point) {
|
| 115 |
+
const canvasCoord = coordManager.dataToCanvas(point[0], point[1]);
|
| 116 |
+
const distance = Math.sqrt(
|
| 117 |
+
Math.pow(mouseX - canvasCoord.x, 2) +
|
| 118 |
+
Math.pow(mouseY - canvasCoord.y, 2)
|
| 119 |
+
);
|
| 120 |
+
|
| 121 |
+
if (distance <= hitRadius) {
|
| 122 |
+
return {
|
| 123 |
+
type: 'body',
|
| 124 |
+
index: i,
|
| 125 |
+
x: point[0],
|
| 126 |
+
y: point[1]
|
| 127 |
+
};
|
| 128 |
+
}
|
| 129 |
+
}
|
| 130 |
+
}
|
| 131 |
+
}
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
// 手のキーポイントをチェック(詳細モード時)
|
| 135 |
+
if (currentEditMode === 'detailed' && currentDrawHands && poseData.hands) {
|
| 136 |
+
for (let handIndex = 0; handIndex < poseData.hands.length; handIndex++) {
|
| 137 |
+
const hand = poseData.hands[handIndex];
|
| 138 |
+
if (hand) {
|
| 139 |
+
for (let i = 0; i < hand.length; i += 3) {
|
| 140 |
+
const x = hand[i];
|
| 141 |
+
const y = hand[i + 1];
|
| 142 |
+
const conf = hand[i + 2];
|
| 143 |
+
|
| 144 |
+
if (conf > 0.3) {
|
| 145 |
+
const canvasCoord = coordManager.dataToCanvas(x, y);
|
| 146 |
+
const distance = Math.sqrt(
|
| 147 |
+
Math.pow(mouseX - canvasCoord.x, 2) +
|
| 148 |
+
Math.pow(mouseY - canvasCoord.y, 2)
|
| 149 |
+
);
|
| 150 |
+
|
| 151 |
+
if (distance <= hitRadius) {
|
| 152 |
+
return {
|
| 153 |
+
type: 'hand',
|
| 154 |
+
handIndex: handIndex,
|
| 155 |
+
index: i / 3,
|
| 156 |
+
x: x,
|
| 157 |
+
y: y
|
| 158 |
+
};
|
| 159 |
+
}
|
| 160 |
+
}
|
| 161 |
+
}
|
| 162 |
+
}
|
| 163 |
+
}
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
// 顔のキーポイントをチェック(詳細モード時)
|
| 167 |
+
if (currentEditMode === 'detailed' && currentDrawFace && poseData.faces) {
|
| 168 |
+
const face = poseData.faces[0];
|
| 169 |
+
if (face) {
|
| 170 |
+
for (let i = 0; i < face.length; i += 3) {
|
| 171 |
+
const x = face[i];
|
| 172 |
+
const y = face[i + 1];
|
| 173 |
+
const conf = face[i + 2];
|
| 174 |
+
|
| 175 |
+
if (conf > 0.3) {
|
| 176 |
+
const canvasCoord = coordManager.dataToCanvas(x, y);
|
| 177 |
+
const distance = Math.sqrt(
|
| 178 |
+
Math.pow(mouseX - canvasCoord.x, 2) +
|
| 179 |
+
Math.pow(mouseY - canvasCoord.y, 2)
|
| 180 |
+
);
|
| 181 |
+
|
| 182 |
+
if (distance <= hitRadius) {
|
| 183 |
+
return {
|
| 184 |
+
type: 'face',
|
| 185 |
+
index: i / 3,
|
| 186 |
+
x: x,
|
| 187 |
+
y: y
|
| 188 |
+
};
|
| 189 |
+
}
|
| 190 |
+
}
|
| 191 |
+
}
|
| 192 |
+
}
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
return null;
|
| 196 |
+
}
|
| 197 |
+
```
|
| 198 |
+
|
| 199 |
+
### 3. マウスイベントハンドラ
|
| 200 |
+
```javascript
|
| 201 |
+
// マウスイベントの設定
|
| 202 |
+
function setupMouseEvents() {
|
| 203 |
+
if (!canvas) return;
|
| 204 |
+
|
| 205 |
+
canvas.addEventListener('mousedown', handleMouseDown);
|
| 206 |
+
canvas.addEventListener('mousemove', handleMouseMove);
|
| 207 |
+
canvas.addEventListener('mouseup', handleMouseUp);
|
| 208 |
+
canvas.addEventListener('mouseleave', handleMouseLeave);
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
function handleMouseDown(event) {
|
| 212 |
+
if (!isCanvasReady()) return;
|
| 213 |
+
|
| 214 |
+
const rect = canvas.getBoundingClientRect();
|
| 215 |
+
const mouseX = event.clientX - rect.left;
|
| 216 |
+
const mouseY = event.clientY - rect.top;
|
| 217 |
+
|
| 218 |
+
const target = getKeypointAt(mouseX, mouseY);
|
| 219 |
+
if (target && currentEditMode === 'detailed') {
|
| 220 |
+
dragManager.startDrag(target, mouseX, mouseY);
|
| 221 |
+
canvas.style.cursor = 'grabbing';
|
| 222 |
+
event.preventDefault();
|
| 223 |
+
}
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
function handleMouseMove(event) {
|
| 227 |
+
if (!isCanvasReady()) return;
|
| 228 |
+
|
| 229 |
+
const rect = canvas.getBoundingClientRect();
|
| 230 |
+
const mouseX = event.clientX - rect.left;
|
| 231 |
+
const mouseY = event.clientY - rect.top;
|
| 232 |
+
|
| 233 |
+
if (dragManager.isDragging) {
|
| 234 |
+
// ドラッグ中
|
| 235 |
+
if (dragManager.updateDrag(mouseX, mouseY)) {
|
| 236 |
+
redrawCanvas();
|
| 237 |
+
}
|
| 238 |
+
} else {
|
| 239 |
+
// ホバー処理
|
| 240 |
+
const target = getKeypointAt(mouseX, mouseY);
|
| 241 |
+
if (target && currentEditMode === 'detailed') {
|
| 242 |
+
canvas.style.cursor = 'grab';
|
| 243 |
+
dragManager.hoverTarget = target;
|
| 244 |
+
} else {
|
| 245 |
+
canvas.style.cursor = 'default';
|
| 246 |
+
dragManager.hoverTarget = null;
|
| 247 |
+
}
|
| 248 |
+
}
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
function handleMouseUp(event) {
|
| 252 |
+
if (dragManager.isDragging) {
|
| 253 |
+
dragManager.endDrag();
|
| 254 |
+
canvas.style.cursor = 'default';
|
| 255 |
+
|
| 256 |
+
// Gradioにポーズデータを送信
|
| 257 |
+
if (window.notifyPoseDataChanged) {
|
| 258 |
+
window.notifyPoseDataChanged(poseData);
|
| 259 |
+
}
|
| 260 |
+
}
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
function handleMouseLeave(event) {
|
| 264 |
+
dragManager.endDrag();
|
| 265 |
+
canvas.style.cursor = 'default';
|
| 266 |
+
}
|
| 267 |
+
```
|
| 268 |
+
|
| 269 |
+
### 4. ホバー時の視覚フィードバック
|
| 270 |
+
```javascript
|
| 271 |
+
// ホバー時のキーポイント強調表示
|
| 272 |
+
function drawKeypointHighlight(x, y, radius = KEYPOINT_RADIUS) {
|
| 273 |
+
const canvasCoord = coordManager.dataToCanvas(x, y);
|
| 274 |
+
|
| 275 |
+
// 外側の輪
|
| 276 |
+
ctx.save();
|
| 277 |
+
ctx.strokeStyle = '#ffffff';
|
| 278 |
+
ctx.lineWidth = 3;
|
| 279 |
+
ctx.beginPath();
|
| 280 |
+
ctx.arc(canvasCoord.x, canvasCoord.y, radius + 4, 0, Math.PI * 2);
|
| 281 |
+
ctx.stroke();
|
| 282 |
+
|
| 283 |
+
// 内側の輪
|
| 284 |
+
ctx.strokeStyle = '#000000';
|
| 285 |
+
ctx.lineWidth = 1;
|
| 286 |
+
ctx.beginPath();
|
| 287 |
+
ctx.arc(canvasCoord.x, canvasCoord.y, radius + 4, 0, Math.PI * 2);
|
| 288 |
+
ctx.stroke();
|
| 289 |
+
ctx.restore();
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
// 描画関数にホバー表示を追加
|
| 293 |
+
function redrawCanvasWithHover() {
|
| 294 |
+
redrawCanvas();
|
| 295 |
+
|
| 296 |
+
// ホバーターゲットがあれば強調表示
|
| 297 |
+
if (dragManager.hoverTarget && !dragManager.isDragging) {
|
| 298 |
+
drawKeypointHighlight(
|
| 299 |
+
dragManager.hoverTarget.x,
|
| 300 |
+
dragManager.hoverTarget.y
|
| 301 |
+
);
|
| 302 |
+
}
|
| 303 |
+
}
|
| 304 |
+
```
|
| 305 |
+
|
| 306 |
+
### 5. デバウンス処理
|
| 307 |
+
```javascript
|
| 308 |
+
// ドラッグ更新のデバウンス
|
| 309 |
+
let dragUpdateTimeout = null;
|
| 310 |
+
|
| 311 |
+
function debouncedDragUpdate() {
|
| 312 |
+
if (dragUpdateTimeout) {
|
| 313 |
+
clearTimeout(dragUpdateTimeout);
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
dragUpdateTimeout = setTimeout(() => {
|
| 317 |
+
if (window.notifyPoseDataChanged) {
|
| 318 |
+
window.notifyPoseDataChanged(poseData);
|
| 319 |
+
}
|
| 320 |
+
}, 100); // 100ms後に更新
|
| 321 |
+
}
|
| 322 |
+
```
|
| 323 |
+
|
| 324 |
+
## 参考
|
| 325 |
+
- `./refs/dwpose_modifier/issues/021_ドラッグ時座標ジャンプ修正_complete.md`
|
| 326 |
+
- 特にオフセット計算とドラッグ精度の改善
|
| 327 |
+
|
| 328 |
+
## 完了条件
|
| 329 |
+
- [ ] キーポイントがマウスでドラッグできる
|
| 330 |
+
- [ ] ドラッグ時に座標ジャンプが発生しない
|
| 331 |
+
- [ ] ホバー時に視覚フィードバックがある
|
| 332 |
+
- [ ] 詳細モードでのみドラッグが有効
|
| 333 |
+
- [ ] ドラッグ後にポーズデータが更新される
|
issues/010_手描画チェックボックス.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Issue: 手描画チェックボックス
|
| 2 |
+
|
| 3 |
+
## 概要
|
| 4 |
+
手のキーポイントの表示/非表示を切り替えるチェックボックス機能を実装。
|
| 5 |
+
|
| 6 |
+
## 実装内容
|
| 7 |
+
|
| 8 |
+
### 1. Gradioイベントハンドラ
|
| 9 |
+
```python
|
| 10 |
+
def toggle_hand_drawing(show_hands):
|
| 11 |
+
"""手描画の切り替え"""
|
| 12 |
+
update_js = f"""
|
| 13 |
+
<script>
|
| 14 |
+
setTimeout(() => {{
|
| 15 |
+
if (window.setHandDrawing) {{
|
| 16 |
+
window.setHandDrawing({json.dumps(show_hands)});
|
| 17 |
+
}}
|
| 18 |
+
}}, 50);
|
| 19 |
+
</script>
|
| 20 |
+
"""
|
| 21 |
+
return update_js
|
| 22 |
+
|
| 23 |
+
# UIイベント設定
|
| 24 |
+
draw_hand.change(
|
| 25 |
+
toggle_hand_drawing,
|
| 26 |
+
inputs=[draw_hand],
|
| 27 |
+
outputs=[js_executor]
|
| 28 |
+
)
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
### 2. JavaScript実装
|
| 32 |
+
```javascript
|
| 33 |
+
let currentDrawHands = true;
|
| 34 |
+
|
| 35 |
+
window.setHandDrawing = function(showHands) {
|
| 36 |
+
currentDrawHands = showHands;
|
| 37 |
+
debugLog(`Hand drawing: ${showHands}`);
|
| 38 |
+
redrawCanvas();
|
| 39 |
+
};
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
## 完了条件
|
| 43 |
+
- [ ] チェックボックスで手の表示/非表示が切り替わる
|
| 44 |
+
- [ ] リアルタイムで反映される
|
issues/011_顔描画チェックボックス.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Issue: 顔描画チェックボックス
|
| 2 |
+
|
| 3 |
+
## 概要
|
| 4 |
+
顔のキーポイントの表示/非表示を切り替えるチェックボックス機能を実装。
|
| 5 |
+
|
| 6 |
+
## 実装内容
|
| 7 |
+
|
| 8 |
+
### 1. Gradioイベントハンドラ
|
| 9 |
+
```python
|
| 10 |
+
def toggle_face_drawing(show_face):
|
| 11 |
+
"""顔描画の切り替え"""
|
| 12 |
+
update_js = f"""
|
| 13 |
+
<script>
|
| 14 |
+
setTimeout(() => {{
|
| 15 |
+
if (window.setFaceDrawing) {{
|
| 16 |
+
window.setFaceDrawing({json.dumps(show_face)});
|
| 17 |
+
}}
|
| 18 |
+
}}, 50);
|
| 19 |
+
</script>
|
| 20 |
+
"""
|
| 21 |
+
return update_js
|
| 22 |
+
|
| 23 |
+
# UIイベント設定
|
| 24 |
+
draw_face.change(
|
| 25 |
+
toggle_face_drawing,
|
| 26 |
+
inputs=[draw_face],
|
| 27 |
+
outputs=[js_executor]
|
| 28 |
+
)
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
### 2. JavaScript実装
|
| 32 |
+
```javascript
|
| 33 |
+
let currentDrawFace = true;
|
| 34 |
+
|
| 35 |
+
window.setFaceDrawing = function(showFace) {
|
| 36 |
+
currentDrawFace = showFace;
|
| 37 |
+
debugLog(`Face drawing: ${showFace}`);
|
| 38 |
+
redrawCanvas();
|
| 39 |
+
};
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
## 完了条件
|
| 43 |
+
- [ ] チェックボックスで顔の表示/非表示が切り替わる
|
| 44 |
+
- [ ] 手描画とは独立して動作する
|
issues/012_簡易モード矩形選択.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Issue: 簡易モード矩形選択
|
| 2 |
+
|
| 3 |
+
## 概要
|
| 4 |
+
簡易モードで手と顔を矩形として表示し、矩形クリックで編集モードに入る機能を実装。
|
| 5 |
+
|
| 6 |
+
## 背景
|
| 7 |
+
dwpose_modifierのタブ1にある機能。手21キーポイントと顔68キーポイントを矩形で代表し、直感的な編集を可能にする。
|
| 8 |
+
|
| 9 |
+
## 実装内容
|
| 10 |
+
|
| 11 |
+
### 1. 矩形計算ユーティリティ
|
| 12 |
+
```javascript
|
| 13 |
+
function calculateHandRectangle(handData) {
|
| 14 |
+
if (!handData || handData.length === 0) return null;
|
| 15 |
+
|
| 16 |
+
let minX = Infinity, minY = Infinity;
|
| 17 |
+
let maxX = -Infinity, maxY = -Infinity;
|
| 18 |
+
|
| 19 |
+
for (let i = 0; i < handData.length; i += 3) {
|
| 20 |
+
const x = handData[i];
|
| 21 |
+
const y = handData[i + 1];
|
| 22 |
+
const conf = handData[i + 2];
|
| 23 |
+
|
| 24 |
+
if (conf > 0.3) {
|
| 25 |
+
minX = Math.min(minX, x);
|
| 26 |
+
minY = Math.min(minY, y);
|
| 27 |
+
maxX = Math.max(maxX, x);
|
| 28 |
+
maxY = Math.max(maxY, y);
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
if (minX === Infinity) return null;
|
| 33 |
+
|
| 34 |
+
// マージンを追加
|
| 35 |
+
const margin = 10;
|
| 36 |
+
return {
|
| 37 |
+
x: minX - margin,
|
| 38 |
+
y: minY - margin,
|
| 39 |
+
width: maxX - minX + margin * 2,
|
| 40 |
+
height: maxY - minY + margin * 2
|
| 41 |
+
};
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
function calculateFaceRectangle(faceData) {
|
| 45 |
+
// 同様の実装
|
| 46 |
+
}
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
### 2. 矩形描画
|
| 50 |
+
```javascript
|
| 51 |
+
function drawRectangle(rect, color, isSelected = false) {
|
| 52 |
+
const canvasRect = {
|
| 53 |
+
x: coordManager.dataToCanvas(rect.x, 0).x,
|
| 54 |
+
y: coordManager.dataToCanvas(0, rect.y).y,
|
| 55 |
+
width: rect.width * (640 / coordManager.dataWidth),
|
| 56 |
+
height: rect.height * (640 / coordManager.dataHeight)
|
| 57 |
+
};
|
| 58 |
+
|
| 59 |
+
ctx.strokeStyle = color;
|
| 60 |
+
ctx.lineWidth = isSelected ? 3 : 2;
|
| 61 |
+
ctx.setLineDash(isSelected ? [] : [5, 5]);
|
| 62 |
+
ctx.strokeRect(canvasRect.x, canvasRect.y, canvasRect.width, canvasRect.height);
|
| 63 |
+
ctx.setLineDash([]);
|
| 64 |
+
}
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
### 3. 矩形クリック検出
|
| 68 |
+
```javascript
|
| 69 |
+
function getRectangleAt(mouseX, mouseY) {
|
| 70 |
+
if (currentEditMode !== 'simple') return null;
|
| 71 |
+
|
| 72 |
+
// 手の矩形チェック
|
| 73 |
+
if (currentDrawHands && poseData.hands) {
|
| 74 |
+
for (let i = 0; i < poseData.hands.length; i++) {
|
| 75 |
+
const rect = calculateHandRectangle(poseData.hands[i]);
|
| 76 |
+
if (rect && isPointInRectangle(mouseX, mouseY, rect)) {
|
| 77 |
+
return {type: 'hand', index: i, rect: rect};
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
// 顔の矩形チェック
|
| 83 |
+
if (currentDrawFace && poseData.faces) {
|
| 84 |
+
const rect = calculateFaceRectangle(poseData.faces[0]);
|
| 85 |
+
if (rect && isPointInRectangle(mouseX, mouseY, rect)) {
|
| 86 |
+
return {type: 'face', index: 0, rect: rect};
|
| 87 |
+
}
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
return null;
|
| 91 |
+
}
|
| 92 |
+
```
|
| 93 |
+
|
| 94 |
+
## 参考
|
| 95 |
+
- `./refs/dwpose_modifier/issues/028_手顔キーポイント矩形操作機能.md`
|
| 96 |
+
|
| 97 |
+
## 完了条件
|
| 98 |
+
- [ ] 簡易モードで手と顔が矩形表示される
|
| 99 |
+
- [ ] 矩形クリックで選択状態になる
|
| 100 |
+
- [ ] 矩形ドラッグで一括移動できる
|
issues/013_詳細モード個別編集.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Issue: 詳細モード個別編集
|
| 2 |
+
|
| 3 |
+
## 概要
|
| 4 |
+
詳細モードで手と顔の個別キーポイントを直接編集できる機能。Issue #009のドラッグ機能を手と顔に拡張。
|
| 5 |
+
|
| 6 |
+
## 実装内容
|
| 7 |
+
|
| 8 |
+
### 1. 詳細モード描画
|
| 9 |
+
```javascript
|
| 10 |
+
function drawDetailedHands(handsData) {
|
| 11 |
+
if (!handsData || !currentDrawHands) return;
|
| 12 |
+
|
| 13 |
+
ctx.fillStyle = POSE_COLORS.hand;
|
| 14 |
+
|
| 15 |
+
handsData.forEach((hand, handIndex) => {
|
| 16 |
+
if (hand && hand.length > 0) {
|
| 17 |
+
for (let i = 0; i < hand.length; i += 3) {
|
| 18 |
+
const x = hand[i];
|
| 19 |
+
const y = hand[i + 1];
|
| 20 |
+
const conf = hand[i + 2];
|
| 21 |
+
|
| 22 |
+
if (conf > 0.3) {
|
| 23 |
+
drawKeypointTransformed(x, y, 3);
|
| 24 |
+
}
|
| 25 |
+
}
|
| 26 |
+
}
|
| 27 |
+
});
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
function drawDetailedFace(facesData) {
|
| 31 |
+
if (!facesData || !currentDrawFace) return;
|
| 32 |
+
|
| 33 |
+
ctx.fillStyle = POSE_COLORS.face;
|
| 34 |
+
|
| 35 |
+
const face = facesData[0];
|
| 36 |
+
if (face && face.length > 0) {
|
| 37 |
+
for (let i = 0; i < face.length; i += 3) {
|
| 38 |
+
const x = face[i];
|
| 39 |
+
const y = face[i + 1];
|
| 40 |
+
const conf = face[i + 2];
|
| 41 |
+
|
| 42 |
+
if (conf > 0.3) {
|
| 43 |
+
drawKeypointTransformed(x, y, 2);
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
### 2. キーポイント選択表示
|
| 51 |
+
```javascript
|
| 52 |
+
function drawSelectedKeypoint(target) {
|
| 53 |
+
if (!target) return;
|
| 54 |
+
|
| 55 |
+
const canvasCoord = coordManager.dataToCanvas(target.x, target.y);
|
| 56 |
+
|
| 57 |
+
ctx.save();
|
| 58 |
+
ctx.strokeStyle = '#00ff00';
|
| 59 |
+
ctx.lineWidth = 2;
|
| 60 |
+
ctx.beginPath();
|
| 61 |
+
ctx.arc(canvasCoord.x, canvasCoord.y, 8, 0, Math.PI * 2);
|
| 62 |
+
ctx.stroke();
|
| 63 |
+
ctx.restore();
|
| 64 |
+
}
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
## 完了条件
|
| 68 |
+
- [ ] 詳細モードで手顔の個別キーポイントが表示される
|
| 69 |
+
- [ ] 各キーポイントをドラッグで編集できる
|
| 70 |
+
- [ ] 選択時に視覚フィードバックがある
|
issues/014_モード切替ラジオボタン.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Issue: モード切替ラジオボタン
|
| 2 |
+
|
| 3 |
+
## 概要
|
| 4 |
+
簡易モードと詳細モードを切り替えるラジオボタン機能を実装。
|
| 5 |
+
|
| 6 |
+
## 実装内容
|
| 7 |
+
|
| 8 |
+
### 1. Gradioイベントハンドラ
|
| 9 |
+
```python
|
| 10 |
+
def change_edit_mode(mode):
|
| 11 |
+
"""編集モードの切り替え"""
|
| 12 |
+
update_js = f"""
|
| 13 |
+
<script>
|
| 14 |
+
setTimeout(() => {{
|
| 15 |
+
if (window.setEditMode) {{
|
| 16 |
+
window.setEditMode('{mode}');
|
| 17 |
+
}}
|
| 18 |
+
}}, 50);
|
| 19 |
+
</script>
|
| 20 |
+
"""
|
| 21 |
+
return update_js
|
| 22 |
+
|
| 23 |
+
edit_mode.change(
|
| 24 |
+
change_edit_mode,
|
| 25 |
+
inputs=[edit_mode],
|
| 26 |
+
outputs=[js_executor]
|
| 27 |
+
)
|
| 28 |
+
```
|
| 29 |
+
|
| 30 |
+
### 2. JavaScript実装
|
| 31 |
+
```javascript
|
| 32 |
+
let currentEditMode = 'simple';
|
| 33 |
+
|
| 34 |
+
window.setEditMode = function(mode) {
|
| 35 |
+
currentEditMode = mode;
|
| 36 |
+
debugLog(`Edit mode changed to: ${mode}`);
|
| 37 |
+
|
| 38 |
+
// ドラッグ状態をリセット
|
| 39 |
+
dragManager.endDrag();
|
| 40 |
+
|
| 41 |
+
// カーソルをリセット
|
| 42 |
+
if (canvas) {
|
| 43 |
+
canvas.style.cursor = 'default';
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
redrawCanvas();
|
| 47 |
+
};
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
## 完了条件
|
| 51 |
+
- [ ] ラジオボタンでモード切替ができる
|
| 52 |
+
- [ ] モード切替時に表示が更新される
|
| 53 |
+
- [ ] ドラッグ状態が適切にリセットされる
|
issues/015_Canvas解像度変更機能.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Issue: Canvas解像度変更機能
|
| 2 |
+
|
| 3 |
+
## 概要
|
| 4 |
+
Canvasのデータ解像度を変更する機能。表示サイズは640x640固定、データ解像度のみ変更。
|
| 5 |
+
|
| 6 |
+
## 実装内容
|
| 7 |
+
|
| 8 |
+
### 1. Gradioイベントハンドラ
|
| 9 |
+
```python
|
| 10 |
+
def update_canvas_resolution(width, height):
|
| 11 |
+
"""Canvas解像度の更新"""
|
| 12 |
+
try:
|
| 13 |
+
width = int(width)
|
| 14 |
+
height = int(height)
|
| 15 |
+
|
| 16 |
+
if width < 64 or width > 2048 or height < 64 or height > 2048:
|
| 17 |
+
return gr.Warning("解像度は64-2048の範囲で設定してください")
|
| 18 |
+
|
| 19 |
+
update_js = f"""
|
| 20 |
+
<script>
|
| 21 |
+
setTimeout(() => {{
|
| 22 |
+
if (window.updateCanvasResolution) {{
|
| 23 |
+
window.updateCanvasResolution({width}, {height});
|
| 24 |
+
}}
|
| 25 |
+
}}, 50);
|
| 26 |
+
</script>
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
return update_js, gr.Info(f"解像度を{width}x{height}に変更しました")
|
| 30 |
+
|
| 31 |
+
except ValueError:
|
| 32 |
+
return None, gr.Error("無効な解像度値です")
|
| 33 |
+
|
| 34 |
+
update_canvas_btn.click(
|
| 35 |
+
update_canvas_resolution,
|
| 36 |
+
inputs=[canvas_width, canvas_height],
|
| 37 |
+
outputs=[js_executor, None]
|
| 38 |
+
)
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
### 2. JavaScript実装
|
| 42 |
+
```javascript
|
| 43 |
+
window.updateCanvasResolution = function(width, height) {
|
| 44 |
+
debugLog(`Updating canvas resolution to ${width}x${height}`);
|
| 45 |
+
|
| 46 |
+
// 座標マネージャーの解像度を更新
|
| 47 |
+
coordManager.setDataResolution(width, height);
|
| 48 |
+
|
| 49 |
+
// 既存のポーズデータがあれば再スケール
|
| 50 |
+
if (poseData) {
|
| 51 |
+
rescalePoseData(poseData, width, height);
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
redrawCanvas();
|
| 55 |
+
};
|
| 56 |
+
|
| 57 |
+
function rescalePoseData(pose_data, newWidth, newHeight) {
|
| 58 |
+
const oldWidth = coordManager.dataWidth;
|
| 59 |
+
const oldHeight = coordManager.dataHeight;
|
| 60 |
+
|
| 61 |
+
const scaleX = newWidth / oldWidth;
|
| 62 |
+
const scaleY = newHeight / oldHeight;
|
| 63 |
+
|
| 64 |
+
// ボディキーポイントのスケール
|
| 65 |
+
if (pose_data.bodies && pose_data.bodies.candidate) {
|
| 66 |
+
pose_data.bodies.candidate.forEach(point => {
|
| 67 |
+
if (point && point.length >= 2) {
|
| 68 |
+
point[0] *= scaleX;
|
| 69 |
+
point[1] *= scaleY;
|
| 70 |
+
}
|
| 71 |
+
});
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
// 手と顔も同様にスケール
|
| 75 |
+
// ...
|
| 76 |
+
}
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
## 完了条件
|
| 80 |
+
- [ ] 解像度変更ボタンが機能する
|
| 81 |
+
- [ ] 既存ポーズデータが新解像度にスケールされる
|
| 82 |
+
- [ ] 範囲外の値でエラー表示される
|
issues/016_テンプレートポーズ選択.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Issue: テンプレートポーズ選択
|
| 2 |
+
|
| 3 |
+
## 概要
|
| 4 |
+
2頭身・3頭身のテンプレートポーズを選択してCanvasに読み込む機能を実装。
|
| 5 |
+
|
| 6 |
+
## 実装内容
|
| 7 |
+
|
| 8 |
+
### 1. テンプレートデータ準備
|
| 9 |
+
`templates/poses.json`:
|
| 10 |
+
```json
|
| 11 |
+
{
|
| 12 |
+
"2頭身ポーズ画像 2頭身": {
|
| 13 |
+
"bodies": {
|
| 14 |
+
"candidate": [[100, 50], [120, 80], ...],
|
| 15 |
+
"subset": [[0, 1, 2, ...]]
|
| 16 |
+
},
|
| 17 |
+
"hands": [],
|
| 18 |
+
"faces": [],
|
| 19 |
+
"resolution": [512, 512]
|
| 20 |
+
},
|
| 21 |
+
"3頭身ポーズ画像 3頭身": {
|
| 22 |
+
"bodies": {
|
| 23 |
+
"candidate": [[100, 40], [115, 70], ...],
|
| 24 |
+
"subset": [[0, 1, 2, ...]]
|
| 25 |
+
},
|
| 26 |
+
"hands": [],
|
| 27 |
+
"faces": [],
|
| 28 |
+
"resolution": [512, 512]
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
### 2. Gradioイベントハンドラ
|
| 34 |
+
```python
|
| 35 |
+
def load_template_pose(template_name):
|
| 36 |
+
"""テンプレートポーズの読み込み"""
|
| 37 |
+
try:
|
| 38 |
+
with open("templates/poses.json", "r", encoding="utf-8") as f:
|
| 39 |
+
templates = json.load(f)
|
| 40 |
+
|
| 41 |
+
if template_name not in templates:
|
| 42 |
+
return None, gr.Warning("テンプレートが見つかりません")
|
| 43 |
+
|
| 44 |
+
pose_data = templates[template_name]
|
| 45 |
+
|
| 46 |
+
update_js = f"""
|
| 47 |
+
<script>
|
| 48 |
+
setTimeout(() => {{
|
| 49 |
+
if (window.loadTemplatepose) {{
|
| 50 |
+
window.loadTemplatePose({json.dumps(pose_data)});
|
| 51 |
+
}}
|
| 52 |
+
}}, 50);
|
| 53 |
+
</script>
|
| 54 |
+
"""
|
| 55 |
+
|
| 56 |
+
return update_js, gr.Info(f"{template_name}を読み込みました")
|
| 57 |
+
|
| 58 |
+
except Exception as e:
|
| 59 |
+
return None, gr.Error(f"テンプレート読み込みエラー: {str(e)}")
|
| 60 |
+
|
| 61 |
+
template_dropdown.change(
|
| 62 |
+
load_template_pose,
|
| 63 |
+
inputs=[template_dropdown],
|
| 64 |
+
outputs=[js_executor, None]
|
| 65 |
+
)
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
### 3. JavaScript実装
|
| 69 |
+
```javascript
|
| 70 |
+
window.loadTemplatePose = function(templateData) {
|
| 71 |
+
debugLog("Loading template pose");
|
| 72 |
+
|
| 73 |
+
// 背景画像をクリア
|
| 74 |
+
backgroundImage = null;
|
| 75 |
+
backgroundImageInfo = null;
|
| 76 |
+
|
| 77 |
+
// ポーズデータを設定
|
| 78 |
+
poseData = templateData;
|
| 79 |
+
|
| 80 |
+
// 解像度を更新
|
| 81 |
+
if (templateData.resolution) {
|
| 82 |
+
coordManager.setDataResolution(templateData.resolution[0], templateData.resolution[1]);
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
redrawCanvas();
|
| 86 |
+
};
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
## 完了条件
|
| 90 |
+
- [ ] ドロップダウンでテンプレート選択できる
|
| 91 |
+
- [ ] 選択したテンプレートがCanvasに表示される
|
| 92 |
+
- [ ] 背景画像がクリアされる
|
issues/017_ポーズ画像エクスポート.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Issue: ポーズ画像エクスポート
|
| 2 |
+
|
| 3 |
+
## 概要
|
| 4 |
+
編集したポーズをPNG画像としてエクスポートする機能を実装。
|
| 5 |
+
|
| 6 |
+
## 実装内容
|
| 7 |
+
|
| 8 |
+
### 1. Pythonでの画像生成
|
| 9 |
+
```python
|
| 10 |
+
def generate_pose_image(pose_data, width=512, height=512):
|
| 11 |
+
"""ポーズデータから画像を生成"""
|
| 12 |
+
try:
|
| 13 |
+
from PIL import Image, ImageDraw
|
| 14 |
+
import numpy as np
|
| 15 |
+
|
| 16 |
+
# 白背景の画像作成
|
| 17 |
+
img = Image.new('RGB', (width, height), 'white')
|
| 18 |
+
draw = ImageDraw.Draw(img)
|
| 19 |
+
|
| 20 |
+
# ポーズ描画ロジック
|
| 21 |
+
if pose_data and 'bodies' in pose_data:
|
| 22 |
+
# ボディ描画
|
| 23 |
+
draw_body_on_image(draw, pose_data['bodies'], width, height)
|
| 24 |
+
|
| 25 |
+
# 手描画
|
| 26 |
+
if 'hands' in pose_data:
|
| 27 |
+
draw_hands_on_image(draw, pose_data['hands'], width, height)
|
| 28 |
+
|
| 29 |
+
# 顔描画
|
| 30 |
+
if 'faces' in pose_data:
|
| 31 |
+
draw_face_on_image(draw, pose_data['faces'], width, height)
|
| 32 |
+
|
| 33 |
+
return img
|
| 34 |
+
|
| 35 |
+
except Exception as e:
|
| 36 |
+
return None
|
| 37 |
+
|
| 38 |
+
def export_pose_image(pose_data):
|
| 39 |
+
"""ポーズ画像のエクスポート"""
|
| 40 |
+
if not pose_data:
|
| 41 |
+
return None, gr.Warning("エクスポートするポーズがありません")
|
| 42 |
+
|
| 43 |
+
try:
|
| 44 |
+
img = generate_pose_image(pose_data)
|
| 45 |
+
if img:
|
| 46 |
+
return img, gr.Info("ポーズ画像を生成しました")
|
| 47 |
+
else:
|
| 48 |
+
return None, gr.Error("画像生成に失敗しました")
|
| 49 |
+
except Exception as e:
|
| 50 |
+
return None, gr.Error(f"エクスポートエラー: {str(e)}")
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
### 2. JavaScript連携
|
| 54 |
+
```javascript
|
| 55 |
+
window.requestPoseExport = function() {
|
| 56 |
+
if (!poseData) {
|
| 57 |
+
debugLog("No pose data to export");
|
| 58 |
+
return;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
// Gradioにポーズデータを送信してエクスポート要求
|
| 62 |
+
if (window.triggerPoseExport) {
|
| 63 |
+
window.triggerPoseExport(poseData);
|
| 64 |
+
}
|
| 65 |
+
};
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
## 完了条件
|
| 69 |
+
- [ ] ポーズがPNG画像として生成される
|
| 70 |
+
- [ ] ダウンロードボタンが機能する
|
| 71 |
+
- [ ] 解像度に応じて正しくスケールされる
|
issues/018_JSONデータエクスポート.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Issue: JSONデータエクスポート
|
| 2 |
+
|
| 3 |
+
## 概要
|
| 4 |
+
編集したポーズデータをJSON形式でエクスポートする機能を実装。
|
| 5 |
+
|
| 6 |
+
## 実装内容
|
| 7 |
+
|
| 8 |
+
### 1. Gradioエクスポート機能
|
| 9 |
+
```python
|
| 10 |
+
def export_pose_json(pose_data):
|
| 11 |
+
"""ポーズデータのJSONエクスポート"""
|
| 12 |
+
if not pose_data:
|
| 13 |
+
return None, gr.Warning("エクスポートするポーズがありません")
|
| 14 |
+
|
| 15 |
+
try:
|
| 16 |
+
# JSONファイルとして保存
|
| 17 |
+
import tempfile
|
| 18 |
+
import json
|
| 19 |
+
|
| 20 |
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
| 21 |
+
json.dump(pose_data, f, indent=2, ensure_ascii=False)
|
| 22 |
+
temp_path = f.name
|
| 23 |
+
|
| 24 |
+
return temp_path, gr.Info("JSONファイルを生成しました")
|
| 25 |
+
|
| 26 |
+
except Exception as e:
|
| 27 |
+
return None, gr.Error(f"JSON生成エラー: {str(e)}")
|
| 28 |
+
|
| 29 |
+
# UIボタンの設定
|
| 30 |
+
download_json_btn.click(
|
| 31 |
+
export_pose_json,
|
| 32 |
+
inputs=[pose_data_state],
|
| 33 |
+
outputs=[json_download_file, None]
|
| 34 |
+
)
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
### 2. JavaScript連携
|
| 38 |
+
```javascript
|
| 39 |
+
window.requestJsonExport = function() {
|
| 40 |
+
if (!poseData) {
|
| 41 |
+
debugLog("No pose data to export");
|
| 42 |
+
return;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
// Gradioに現在のポーズデータを送信
|
| 46 |
+
if (window.triggerJsonExport) {
|
| 47 |
+
window.triggerJsonExport(poseData);
|
| 48 |
+
}
|
| 49 |
+
};
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
### 3. リアルタイム表示
|
| 53 |
+
```python
|
| 54 |
+
def update_json_display(pose_data):
|
| 55 |
+
"""JSON表示の更新"""
|
| 56 |
+
if pose_data:
|
| 57 |
+
return gr.update(value=pose_data)
|
| 58 |
+
else:
|
| 59 |
+
return gr.update(value={})
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
## 完了条件
|
| 63 |
+
- [ ] ポーズデータがJSON形式で表示される
|
| 64 |
+
- [ ] JSONファイルとしてダウンロードできる
|
| 65 |
+
- [ ] 編集時にリアルタイムで更新される
|
issues/019_エラーハンドリング統合.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Issue: エラーハンドリング統合
|
| 2 |
+
|
| 3 |
+
## 概要
|
| 4 |
+
アプリケーション全体のエラーハンドリングを統合し、ユーザーフレンドリーなエラー表示を実現。
|
| 5 |
+
|
| 6 |
+
## 実装内容
|
| 7 |
+
|
| 8 |
+
### 1. エラー分類と処理
|
| 9 |
+
```python
|
| 10 |
+
class DWPoseEditorError(Exception):
|
| 11 |
+
"""アプリケーション固有のエラー基底クラス"""
|
| 12 |
+
pass
|
| 13 |
+
|
| 14 |
+
class ModelLoadError(DWPoseEditorError):
|
| 15 |
+
"""モデル読み込みエラー"""
|
| 16 |
+
pass
|
| 17 |
+
|
| 18 |
+
class PoseDetectionError(DWPoseEditorError):
|
| 19 |
+
"""ポーズ検出エラー"""
|
| 20 |
+
pass
|
| 21 |
+
|
| 22 |
+
class ImageProcessingError(DWPoseEditorError):
|
| 23 |
+
"""画像処理エラー"""
|
| 24 |
+
pass
|
| 25 |
+
|
| 26 |
+
def handle_error(error, context=""):
|
| 27 |
+
"""統一エラーハンドラ"""
|
| 28 |
+
if isinstance(error, ModelLoadError):
|
| 29 |
+
return gr.Error("モデルの読み込みに失敗しました。しばらく待ってから再試行してください。")
|
| 30 |
+
elif isinstance(error, PoseDetectionError):
|
| 31 |
+
return gr.Warning("ポーズを検出できませんでした。別の画像をお試しください。")
|
| 32 |
+
elif isinstance(error, ImageProcessingError):
|
| 33 |
+
return gr.Error("画像の処理中にエラーが発生しました。")
|
| 34 |
+
else:
|
| 35 |
+
return gr.Error(f"予期しないエラーが発生しました: {str(error)}")
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
### 2. JavaScript エラーハンドリング
|
| 39 |
+
```javascript
|
| 40 |
+
// グローバルエラーハンドラ
|
| 41 |
+
window.addEventListener('error', (event) => {
|
| 42 |
+
debugLog(`Global error: ${event.error.message}`);
|
| 43 |
+
if (isCanvasReady()) {
|
| 44 |
+
showCanvasError('エラーが発生しました');
|
| 45 |
+
}
|
| 46 |
+
});
|
| 47 |
+
|
| 48 |
+
// Promise rejection ハンドラ
|
| 49 |
+
window.addEventListener('unhandledrejection', (event) => {
|
| 50 |
+
debugLog(`Unhandled promise rejection: ${event.reason}`);
|
| 51 |
+
event.preventDefault();
|
| 52 |
+
});
|
| 53 |
+
|
| 54 |
+
// Canvas操作の安全な実行
|
| 55 |
+
function safeExecute(operation, errorMessage = "操作中にエラーが発生しました") {
|
| 56 |
+
try {
|
| 57 |
+
return operation();
|
| 58 |
+
} catch (error) {
|
| 59 |
+
debugLog(`Safe execute error: ${error.message}`);
|
| 60 |
+
if (isCanvasReady()) {
|
| 61 |
+
showCanvasError(errorMessage);
|
| 62 |
+
}
|
| 63 |
+
return null;
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
### 3. ネットワークエラー対応
|
| 69 |
+
```python
|
| 70 |
+
def with_retry(func, max_retries=3):
|
| 71 |
+
"""リトライ機能付きデコレータ"""
|
| 72 |
+
def wrapper(*args, **kwargs):
|
| 73 |
+
for attempt in range(max_retries):
|
| 74 |
+
try:
|
| 75 |
+
return func(*args, **kwargs)
|
| 76 |
+
except Exception as e:
|
| 77 |
+
if attempt == max_retries - 1:
|
| 78 |
+
raise e
|
| 79 |
+
time.sleep(1) # 1秒待機してリトライ
|
| 80 |
+
return None
|
| 81 |
+
return wrapper
|
| 82 |
+
|
| 83 |
+
@with_retry
|
| 84 |
+
def download_model():
|
| 85 |
+
"""モデルダウンロード(リトライ付き)"""
|
| 86 |
+
# DWPoseモデルダウンロード処理
|
| 87 |
+
pass
|
| 88 |
+
```
|
| 89 |
+
|
| 90 |
+
## 完了条件
|
| 91 |
+
- [ ] 各種エラーが適切に分類される
|
| 92 |
+
- [ ] ユーザーに分かりやすいメッセージが表示される
|
| 93 |
+
- [ ] ネットワークエラー時にリトライされる
|
| 94 |
+
- [ ] Canvasエラー時に視覚的にフィードバックされる
|
issues/020_Gradioトースト通知.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Issue: Gradioトースト通知
|
| 2 |
+
|
| 3 |
+
## 概要
|
| 4 |
+
Gradioのトースト通知機能を活用したユーザーフィードバックシステムを実装。
|
| 5 |
+
|
| 6 |
+
## 実装内容
|
| 7 |
+
|
| 8 |
+
### 1. 通知レベル別処理
|
| 9 |
+
```python
|
| 10 |
+
def notify_success(message):
|
| 11 |
+
"""成功通知"""
|
| 12 |
+
return gr.Info(message)
|
| 13 |
+
|
| 14 |
+
def notify_warning(message):
|
| 15 |
+
"""警告通知"""
|
| 16 |
+
return gr.Warning(message)
|
| 17 |
+
|
| 18 |
+
def notify_error(message):
|
| 19 |
+
"""エラー通知"""
|
| 20 |
+
return gr.Error(message)
|
| 21 |
+
|
| 22 |
+
# 使用例
|
| 23 |
+
def handle_image_upload(image):
|
| 24 |
+
try:
|
| 25 |
+
# 処理
|
| 26 |
+
return result, notify_success("画像をアップロードしました")
|
| 27 |
+
except Exception as e:
|
| 28 |
+
return None, notify_error(f"アップロードに失敗しました: {str(e)}")
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
### 2. 進捗通知
|
| 32 |
+
```python
|
| 33 |
+
def notify_progress(message, progress=None):
|
| 34 |
+
"""進捗通知"""
|
| 35 |
+
if progress:
|
| 36 |
+
return gr.Progress(progress, desc=message)
|
| 37 |
+
else:
|
| 38 |
+
return gr.Info(message)
|
| 39 |
+
|
| 40 |
+
# DWPose処理時の進捗表示
|
| 41 |
+
def detect_pose_with_progress(image):
|
| 42 |
+
notify_progress("人物を検出中...", 0.3)
|
| 43 |
+
# YOLOX実行
|
| 44 |
+
|
| 45 |
+
notify_progress("ポーズを解析中...", 0.7)
|
| 46 |
+
# DWPose実行
|
| 47 |
+
|
| 48 |
+
notify_progress("完了", 1.0)
|
| 49 |
+
return result
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
### 3. JavaScriptからの通知
|
| 53 |
+
```javascript
|
| 54 |
+
// Gradioトースト通知のトリガー
|
| 55 |
+
window.showToast = function(type, message) {
|
| 56 |
+
// Gradioの隠しコンポーネントを使って通知
|
| 57 |
+
if (window.triggerToast) {
|
| 58 |
+
window.triggerToast(type, message);
|
| 59 |
+
}
|
| 60 |
+
};
|
| 61 |
+
|
| 62 |
+
// Canvas操作時の通知
|
| 63 |
+
function notifyCanvasOperation(message) {
|
| 64 |
+
showToast('info', message);
|
| 65 |
+
}
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
### 4. 通知設定の管理
|
| 69 |
+
```python
|
| 70 |
+
# 通知設定
|
| 71 |
+
NOTIFICATION_SETTINGS = {
|
| 72 |
+
'show_success': True,
|
| 73 |
+
'show_warnings': True,
|
| 74 |
+
'show_errors': True,
|
| 75 |
+
'auto_dismiss': True,
|
| 76 |
+
'dismiss_timeout': 3000 # 3秒
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
def configure_notifications(settings):
|
| 80 |
+
"""通知設定の更新"""
|
| 81 |
+
global NOTIFICATION_SETTINGS
|
| 82 |
+
NOTIFICATION_SETTINGS.update(settings)
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
## 完了条件
|
| 86 |
+
- [ ] 成功/警告/エラーの通知が適切に表示される
|
| 87 |
+
- [ ] 進捗表示が機能する
|
| 88 |
+
- [ ] JavaScriptからも通知できる
|
| 89 |
+
- [ ] 通知が適切なタイミングで消える
|