Commit
·
4b1e46e
1
Parent(s):
6014789
feat: Enhance pose image export functionality with timestamped filenames and improved drawing logic
Browse files- Added a new function `get_timestamp_filename` to generate filenames with timestamps.
- Updated `export_pose_as_image` to support drawing hands and faces based on new data structures.
- Improved body drawing logic to adhere to the new refs format.
- Enhanced hand and face drawing functions to include coordinate normalization.
- Added detailed debug logging for better traceability during image export.
- Removed redundant success notifications, allowing app.py to handle them.
- app.py +483 -29
- issues/029_手顔表示リアルタイム反映修正.md +38 -0
- issues/030_JSONダウンロードボタン表示修正.md +35 -0
- issues/031_JSONダウンロード保存先修正.md +40 -0
- issues/032_画像ダウンロードボタン動作修正.md +44 -0
- issues/033_詳細モードキーポイント直接編集実装.md +57 -0
- issues/034_画像ダウンロード機能修正.md +91 -0
- issues/035_JSONダウンロード機能修正.md +76 -0
- issues/036_Canvas編集データ反映修正.md +94 -0
- issues/038_Canvas編集データ完全同期修正.md +343 -0
- static/pose_editor.js +1446 -161
- utils/export_utils.py +172 -59
app.py
CHANGED
|
@@ -1,11 +1,21 @@
|
|
| 1 |
import gradio as gr
|
| 2 |
import os
|
|
|
|
| 3 |
from utils.pose_utils import initialize_dwpose, safe_detect_pose
|
| 4 |
from utils.notifications import notify_success, notify_error, NotificationMessages
|
| 5 |
from utils.coordinate_system import update_coordinate_system
|
| 6 |
from utils.image_processing import process_uploaded_image
|
| 7 |
-
from utils.export_utils import export_pose_as_image, export_pose_as_json
|
| 8 |
import json
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
def load_javascript():
|
| 11 |
"""JavaScriptファイルを読み込む"""
|
|
@@ -13,6 +23,21 @@ def load_javascript():
|
|
| 13 |
with open(js_path, "r", encoding="utf-8") as f:
|
| 14 |
return f"<script>{f.read()}</script>"
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
def main():
|
| 17 |
# DWPoseモデル初期化
|
| 18 |
success, message = initialize_dwpose()
|
|
@@ -45,6 +70,13 @@ def main():
|
|
| 45 |
],
|
| 46 |
value="2頭身立ちポーズ"
|
| 47 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
# 中央:エディット部
|
| 50 |
with gr.Column(scale=2):
|
|
@@ -98,33 +130,54 @@ def main():
|
|
| 98 |
|
| 99 |
# 非表示のデータ保持用コンポーネント
|
| 100 |
pose_data = gr.JSON(visible=False, value={})
|
|
|
|
|
|
|
|
|
|
| 101 |
|
| 102 |
# 右側:出力部
|
| 103 |
with gr.Column(scale=1):
|
| 104 |
gr.Markdown("### 出力")
|
| 105 |
|
| 106 |
-
#
|
| 107 |
output_image = gr.Image(
|
| 108 |
label="ポーズ画像",
|
| 109 |
type="pil",
|
| 110 |
-
elem_id="output_image"
|
|
|
|
| 111 |
)
|
| 112 |
|
| 113 |
-
#
|
| 114 |
-
download_image_btn = gr.Button(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
|
| 116 |
-
# JSON
|
| 117 |
output_json = gr.JSON(
|
| 118 |
label="ポーズデータ (JSON)",
|
| 119 |
-
elem_id="output_json"
|
|
|
|
| 120 |
)
|
| 121 |
|
| 122 |
-
# JSON
|
| 123 |
-
download_json_btn = gr.Button(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
|
| 125 |
# イベントハンドラー
|
| 126 |
def on_image_upload(image):
|
| 127 |
-
"""
|
|
|
|
|
|
|
| 128 |
if image is None:
|
| 129 |
return None, {}
|
| 130 |
|
|
@@ -150,10 +203,44 @@ def main():
|
|
| 150 |
zero_count = len([c for c in candidates if c and len(c) >= 2 and (c[0] == 0 or c[1] == 0)])
|
| 151 |
print(f"[DEBUG] ✅ Valid candidates: {valid_count}, 🚫 Zero coordinates: {zero_count}")
|
| 152 |
|
| 153 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
else:
|
| 155 |
print(f"[DEBUG] ❌ Pose detection failed")
|
| 156 |
-
return None, {}
|
| 157 |
|
| 158 |
def on_canvas_size_update(width, height):
|
| 159 |
"""Canvas解像度更新"""
|
|
@@ -189,7 +276,9 @@ def main():
|
|
| 189 |
return gr.update(value=js_code)
|
| 190 |
|
| 191 |
def load_template_pose(template_name):
|
| 192 |
-
"""
|
|
|
|
|
|
|
| 193 |
try:
|
| 194 |
templates_path = os.path.join(os.path.dirname(__file__), "templates", "poses.json")
|
| 195 |
with open(templates_path, "r", encoding="utf-8") as f:
|
|
@@ -205,6 +294,33 @@ def main():
|
|
| 205 |
template_key = template_key_map.get(template_name)
|
| 206 |
if template_key and template_key in templates["poses"]:
|
| 207 |
pose_data = templates["poses"][template_key]["data"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
notify_success(f"{template_name}を読み込みました")
|
| 209 |
return pose_data, pose_data
|
| 210 |
else:
|
|
@@ -215,23 +331,347 @@ def main():
|
|
| 215 |
notify_error(f"テンプレート読み込みに失敗しました: {str(e)}")
|
| 216 |
return None, {}
|
| 217 |
|
| 218 |
-
def export_image(pose_data):
|
| 219 |
-
"""
|
| 220 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
notify_error("エクスポートするポーズデータがありません")
|
| 222 |
-
|
|
|
|
| 223 |
|
| 224 |
-
|
| 225 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
|
| 227 |
def export_json(pose_data):
|
| 228 |
-
"""ポーズJSON
|
| 229 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
notify_error("エクスポートするポーズデータがありません")
|
| 231 |
-
|
|
|
|
| 232 |
|
| 233 |
-
|
| 234 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
|
| 236 |
# 隠しコンポーネント(JavaScript実行用)
|
| 237 |
js_executor = gr.HTML(visible=False, elem_id="js_executor")
|
|
@@ -240,7 +680,7 @@ def main():
|
|
| 240 |
input_image.change(
|
| 241 |
fn=on_image_upload,
|
| 242 |
inputs=[input_image],
|
| 243 |
-
outputs=[output_json, pose_data]
|
| 244 |
)
|
| 245 |
|
| 246 |
# pose_data変更時にCanvas更新(重要!)- 無限ループ防止
|
|
@@ -284,17 +724,31 @@ def main():
|
|
| 284 |
outputs=[output_json, pose_data]
|
| 285 |
)
|
| 286 |
|
| 287 |
-
# エクスポートイベント
|
| 288 |
download_image_btn.click(
|
| 289 |
fn=export_image,
|
| 290 |
-
inputs=[pose_data],
|
| 291 |
-
outputs=[
|
| 292 |
)
|
| 293 |
|
| 294 |
download_json_btn.click(
|
| 295 |
fn=export_json,
|
| 296 |
inputs=[pose_data],
|
| 297 |
-
outputs=[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 298 |
)
|
| 299 |
|
| 300 |
return demo
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
import os
|
| 3 |
+
from PIL import Image
|
| 4 |
from utils.pose_utils import initialize_dwpose, safe_detect_pose
|
| 5 |
from utils.notifications import notify_success, notify_error, NotificationMessages
|
| 6 |
from utils.coordinate_system import update_coordinate_system
|
| 7 |
from utils.image_processing import process_uploaded_image
|
| 8 |
+
from utils.export_utils import export_pose_as_image, export_pose_as_json, get_timestamp_filename
|
| 9 |
import json
|
| 10 |
+
import tempfile
|
| 11 |
+
import base64
|
| 12 |
+
import io
|
| 13 |
+
import time
|
| 14 |
+
|
| 15 |
+
# グローバル変数(refs互換)- 編集中のポーズデータを保持
|
| 16 |
+
_current_poses = None # refsと同じマルチフレーム管理
|
| 17 |
+
_current_frame_index = 0 # 現在編集中のフレーム
|
| 18 |
+
_is_updating = False # Issue 038: データ同期処理中フラグ(refs issue043準拠)
|
| 19 |
|
| 20 |
def load_javascript():
|
| 21 |
"""JavaScriptファイルを読み込む"""
|
|
|
|
| 23 |
with open(js_path, "r", encoding="utf-8") as f:
|
| 24 |
return f"<script>{f.read()}</script>"
|
| 25 |
|
| 26 |
+
def image_to_base64(image):
|
| 27 |
+
"""PIL画像をBase64データURLに変換"""
|
| 28 |
+
if image is None:
|
| 29 |
+
return None
|
| 30 |
+
|
| 31 |
+
# 画像をリサイズ(Canvas表示用に640x640以下に)
|
| 32 |
+
max_size = 640
|
| 33 |
+
image.thumbnail((max_size, max_size), Image.Resampling.LANCZOS)
|
| 34 |
+
|
| 35 |
+
# Base64エンコード
|
| 36 |
+
buffer = io.BytesIO()
|
| 37 |
+
image.save(buffer, format='PNG')
|
| 38 |
+
img_str = base64.b64encode(buffer.getvalue()).decode()
|
| 39 |
+
return f"data:image/png;base64,{img_str}"
|
| 40 |
+
|
| 41 |
def main():
|
| 42 |
# DWPoseモデル初期化
|
| 43 |
success, message = initialize_dwpose()
|
|
|
|
| 70 |
],
|
| 71 |
value="2頭身立ちポーズ"
|
| 72 |
)
|
| 73 |
+
|
| 74 |
+
# JSONファイルアップロード
|
| 75 |
+
json_upload = gr.File(
|
| 76 |
+
label="JSONファイルをロード",
|
| 77 |
+
file_types=[".json"],
|
| 78 |
+
elem_id="json_upload"
|
| 79 |
+
)
|
| 80 |
|
| 81 |
# 中央:エディット部
|
| 82 |
with gr.Column(scale=2):
|
|
|
|
| 130 |
|
| 131 |
# 非表示のデータ保持用コンポーネント
|
| 132 |
pose_data = gr.JSON(visible=False, value={})
|
| 133 |
+
|
| 134 |
+
# JavaScript→Python データ転送用の隠しテキストボックス
|
| 135 |
+
js_pose_update = gr.Textbox(visible=False, elem_id="js_pose_update")
|
| 136 |
|
| 137 |
# 右側:出力部
|
| 138 |
with gr.Column(scale=1):
|
| 139 |
gr.Markdown("### 出力")
|
| 140 |
|
| 141 |
+
# ポーズ画像出力(非表示)
|
| 142 |
output_image = gr.Image(
|
| 143 |
label="ポーズ画像",
|
| 144 |
type="pil",
|
| 145 |
+
elem_id="output_image",
|
| 146 |
+
visible=False
|
| 147 |
)
|
| 148 |
|
| 149 |
+
# ポーズ画像ダウンロード(2クリック方式)
|
| 150 |
+
download_image_btn = gr.Button(
|
| 151 |
+
"📥 画像をダウンロード",
|
| 152 |
+
variant="secondary"
|
| 153 |
+
)
|
| 154 |
+
download_image_file = gr.File(
|
| 155 |
+
label="画像ファイル",
|
| 156 |
+
visible=False
|
| 157 |
+
)
|
| 158 |
|
| 159 |
+
# JSONデータ表示(非表示)
|
| 160 |
output_json = gr.JSON(
|
| 161 |
label="ポーズデータ (JSON)",
|
| 162 |
+
elem_id="output_json",
|
| 163 |
+
visible=False
|
| 164 |
)
|
| 165 |
|
| 166 |
+
# JSONダウンロード(2クリック方式)
|
| 167 |
+
download_json_btn = gr.Button(
|
| 168 |
+
"📥 JSONをダウンロード",
|
| 169 |
+
variant="secondary"
|
| 170 |
+
)
|
| 171 |
+
download_json_file = gr.File(
|
| 172 |
+
label="JSONファイル",
|
| 173 |
+
visible=False
|
| 174 |
+
)
|
| 175 |
|
| 176 |
# イベントハンドラー
|
| 177 |
def on_image_upload(image):
|
| 178 |
+
"""画像アップロード時のポーズ検出(refs互換・マルチフレーム管理)"""
|
| 179 |
+
global _current_poses, _current_frame_index
|
| 180 |
+
|
| 181 |
if image is None:
|
| 182 |
return None, {}
|
| 183 |
|
|
|
|
| 203 |
zero_count = len([c for c in candidates if c and len(c) >= 2 and (c[0] == 0 or c[1] == 0)])
|
| 204 |
print(f"[DEBUG] ✅ Valid candidates: {valid_count}, 🚫 Zero coordinates: {zero_count}")
|
| 205 |
|
| 206 |
+
# refsと同じ_current_poses形式で保存
|
| 207 |
+
if pose_result:
|
| 208 |
+
# people形式に変換
|
| 209 |
+
person_data = {
|
| 210 |
+
"pose_keypoints_2d": [],
|
| 211 |
+
"hand_left_keypoints_2d": pose_result.get('hands', [[], []])[0] if pose_result.get('hands') else [],
|
| 212 |
+
"hand_right_keypoints_2d": pose_result.get('hands', [[], []])[1] if pose_result.get('hands') and len(pose_result['hands']) > 1 else [],
|
| 213 |
+
"face_keypoints_2d": pose_result.get('faces', [[]])[0] if pose_result.get('faces') else []
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
# bodies.candidateからpose_keypoints_2d変換
|
| 217 |
+
if 'bodies' in pose_result and 'candidate' in pose_result['bodies']:
|
| 218 |
+
candidates = pose_result['bodies']['candidate']
|
| 219 |
+
for candidate in candidates:
|
| 220 |
+
if candidate and len(candidate) >= 2:
|
| 221 |
+
person_data["pose_keypoints_2d"].extend([candidate[0], candidate[1], candidate[2] if len(candidate) > 2 else 1.0])
|
| 222 |
+
|
| 223 |
+
_current_poses = [{
|
| 224 |
+
'people': [person_data],
|
| 225 |
+
'metadata': {
|
| 226 |
+
'resolution': pose_result.get('resolution', [512, 512])
|
| 227 |
+
}
|
| 228 |
+
}]
|
| 229 |
+
_current_frame_index = 0
|
| 230 |
+
print(f"[DEBUG] ✅ グローバル変数更新完了(画像アップロード・refs互換)")
|
| 231 |
+
|
| 232 |
+
# 🎨 背景画像をJavaScriptに設定
|
| 233 |
+
image_base64 = image_to_base64(image)
|
| 234 |
+
js_code = f"""
|
| 235 |
+
if (window.setBackgroundImage) {{
|
| 236 |
+
window.setBackgroundImage('{image_base64}');
|
| 237 |
+
}}
|
| 238 |
+
"""
|
| 239 |
+
|
| 240 |
+
return pose_result, pose_result, gr.update(value=js_code)
|
| 241 |
else:
|
| 242 |
print(f"[DEBUG] ❌ Pose detection failed")
|
| 243 |
+
return None, {}, gr.update()
|
| 244 |
|
| 245 |
def on_canvas_size_update(width, height):
|
| 246 |
"""Canvas解像度更新"""
|
|
|
|
| 276 |
return gr.update(value=js_code)
|
| 277 |
|
| 278 |
def load_template_pose(template_name):
|
| 279 |
+
"""テンプレートポーズを読み込み(refs互換・マルチフレーム管理)"""
|
| 280 |
+
global _current_poses, _current_frame_index
|
| 281 |
+
|
| 282 |
try:
|
| 283 |
templates_path = os.path.join(os.path.dirname(__file__), "templates", "poses.json")
|
| 284 |
with open(templates_path, "r", encoding="utf-8") as f:
|
|
|
|
| 294 |
template_key = template_key_map.get(template_name)
|
| 295 |
if template_key and template_key in templates["poses"]:
|
| 296 |
pose_data = templates["poses"][template_key]["data"]
|
| 297 |
+
|
| 298 |
+
# refsと同じ_current_poses形式で保存
|
| 299 |
+
if pose_data:
|
| 300 |
+
# people形式に変換してから保存
|
| 301 |
+
person_data = {
|
| 302 |
+
"pose_keypoints_2d": [],
|
| 303 |
+
"hand_left_keypoints_2d": pose_data.get('hands', [[], []])[0] if pose_data.get('hands') else [],
|
| 304 |
+
"hand_right_keypoints_2d": pose_data.get('hands', [[], []])[1] if pose_data.get('hands') and len(pose_data['hands']) > 1 else [],
|
| 305 |
+
"face_keypoints_2d": pose_data.get('faces', [[]])[0] if pose_data.get('faces') else []
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
# bodies.candidateからpose_keypoints_2d変換
|
| 309 |
+
if 'bodies' in pose_data and 'candidate' in pose_data['bodies']:
|
| 310 |
+
candidates = pose_data['bodies']['candidate']
|
| 311 |
+
for candidate in candidates:
|
| 312 |
+
if candidate and len(candidate) >= 2:
|
| 313 |
+
person_data["pose_keypoints_2d"].extend([candidate[0], candidate[1], candidate[2] if len(candidate) > 2 else 1.0])
|
| 314 |
+
|
| 315 |
+
_current_poses = [{
|
| 316 |
+
'people': [person_data],
|
| 317 |
+
'metadata': {
|
| 318 |
+
'resolution': pose_data.get('resolution', [512, 512])
|
| 319 |
+
}
|
| 320 |
+
}]
|
| 321 |
+
_current_frame_index = 0
|
| 322 |
+
print(f"[DEBUG] ✅ グローバル変数更新完了(テンプレート読み込み・refs互換): {template_name}")
|
| 323 |
+
|
| 324 |
notify_success(f"{template_name}を読み込みました")
|
| 325 |
return pose_data, pose_data
|
| 326 |
else:
|
|
|
|
| 331 |
notify_error(f"テンプレート読み込みに失敗しました: {str(e)}")
|
| 332 |
return None, {}
|
| 333 |
|
| 334 |
+
def export_image(pose_data, draw_hand, draw_face):
|
| 335 |
+
"""ポーズ画像をエクスポート(Button + File方式)(refs互換・マルチフレーム管理)"""
|
| 336 |
+
global _current_poses, _current_frame_index
|
| 337 |
+
|
| 338 |
+
# refsと同じ_current_posesから最新データを取得
|
| 339 |
+
export_data = None
|
| 340 |
+
if _current_poses and 0 <= _current_frame_index < len(_current_poses):
|
| 341 |
+
current_frame = _current_poses[_current_frame_index]
|
| 342 |
+
if current_frame['people'] and current_frame['people'][0]:
|
| 343 |
+
person = current_frame['people'][0]
|
| 344 |
+
|
| 345 |
+
# people形式からbodies.candidate形式に変換(エクスポート用)
|
| 346 |
+
export_data = {
|
| 347 |
+
'bodies': {'candidate': [], 'subset': []},
|
| 348 |
+
'hands': [],
|
| 349 |
+
'faces': [],
|
| 350 |
+
'resolution': current_frame['metadata'].get('resolution', [512, 512])
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
# pose_keypoints_2d をcandidate形式に変換
|
| 354 |
+
if 'pose_keypoints_2d' in person:
|
| 355 |
+
pose_keypoints = person['pose_keypoints_2d']
|
| 356 |
+
for i in range(0, len(pose_keypoints), 3):
|
| 357 |
+
if i + 2 < len(pose_keypoints):
|
| 358 |
+
x = pose_keypoints[i]
|
| 359 |
+
y = pose_keypoints[i + 1]
|
| 360 |
+
conf = pose_keypoints[i + 2]
|
| 361 |
+
export_data['bodies']['candidate'].append([x, y, conf, 0])
|
| 362 |
+
|
| 363 |
+
# 手と顔データ
|
| 364 |
+
left_hand = person.get('hand_left_keypoints_2d', [])
|
| 365 |
+
right_hand = person.get('hand_right_keypoints_2d', [])
|
| 366 |
+
face_data = person.get('face_keypoints_2d', [])
|
| 367 |
+
|
| 368 |
+
export_data['hands'] = [left_hand, right_hand]
|
| 369 |
+
export_data['faces'] = [face_data] if face_data else []
|
| 370 |
+
|
| 371 |
+
# フォールバック:引数のpose_dataを使用
|
| 372 |
+
if export_data is None:
|
| 373 |
+
export_data = pose_data
|
| 374 |
+
|
| 375 |
+
print(f"[DEBUG] 🖼️ 画像ダウンロードボタンクリック")
|
| 376 |
+
print(f"[DEBUG] 📊 グローバルデータ: {bool(_current_poses)}, 引数データ: {bool(pose_data)}")
|
| 377 |
+
print(f"[DEBUG] 📊 使用データ: {bool(export_data)}, 手: {draw_hand}, 顔: {draw_face}")
|
| 378 |
+
|
| 379 |
+
if export_data:
|
| 380 |
+
print(f"[DEBUG] 📊 エクスポートデータキー: {list(export_data.keys()) if isinstance(export_data, dict) else 'not a dict'}")
|
| 381 |
+
if isinstance(export_data, dict):
|
| 382 |
+
for key, value in export_data.items():
|
| 383 |
+
print(f"[DEBUG] 📊 {key}: {type(value)} - {len(value) if isinstance(value, (list, dict)) else value}")
|
| 384 |
+
|
| 385 |
+
# 🚨 グローバルデータの詳細チェック
|
| 386 |
+
if _current_pose_data:
|
| 387 |
+
print(f"[DEBUG] 🔍 グローバルデータ詳細:")
|
| 388 |
+
print(f"[DEBUG] 🔍 - Type: {type(_current_pose_data)}")
|
| 389 |
+
print(f"[DEBUG] 🔍 - Keys: {list(_current_pose_data.keys()) if isinstance(_current_pose_data, dict) else 'Not dict'}")
|
| 390 |
+
if isinstance(_current_pose_data, dict) and 'bodies' in _current_pose_data:
|
| 391 |
+
print(f"[DEBUG] 🔍 - Bodies: {bool(_current_pose_data['bodies'])}")
|
| 392 |
+
if 'candidate' in _current_pose_data['bodies']:
|
| 393 |
+
candidates = _current_pose_data['bodies']['candidate']
|
| 394 |
+
print(f"[DEBUG] 🔍 - Candidates: {len(candidates) if candidates else 0}")
|
| 395 |
+
if isinstance(_current_pose_data, dict) and 'resolution' in _current_pose_data:
|
| 396 |
+
print(f"[DEBUG] 🔍 - Resolution: {_current_pose_data['resolution']}")
|
| 397 |
+
else:
|
| 398 |
+
print(f"[DEBUG] 🔍 グローバルデータがNone")
|
| 399 |
+
|
| 400 |
+
if not export_data:
|
| 401 |
notify_error("エクスポートするポーズデータがありません")
|
| 402 |
+
print(f"[DEBUG] ❌ データなし")
|
| 403 |
+
return gr.update(visible=False)
|
| 404 |
|
| 405 |
+
try:
|
| 406 |
+
# 🎨 表示設定を反映した画像エクスポート��解像度準拠)
|
| 407 |
+
canvas_width = 512 # デフォルト解像度
|
| 408 |
+
canvas_height = 512
|
| 409 |
+
if 'resolution' in export_data and len(export_data['resolution']) >= 2:
|
| 410 |
+
canvas_width = export_data['resolution'][0]
|
| 411 |
+
canvas_height = export_data['resolution'][1]
|
| 412 |
+
|
| 413 |
+
# 🚨 画像生成前のデータ検証
|
| 414 |
+
print(f"[DEBUG] 🎨 画像生成開始: canvas_size=({canvas_width}, {canvas_height})")
|
| 415 |
+
if 'bodies' in export_data and 'candidate' in export_data['bodies']:
|
| 416 |
+
candidates = export_data['bodies']['candidate']
|
| 417 |
+
print(f"[DEBUG] 🎨 Candidates for image: {len(candidates) if candidates else 0}")
|
| 418 |
+
if candidates:
|
| 419 |
+
valid_candidates = [c for c in candidates if c and len(c) >= 2 and c[0] > 0 and c[1] > 0]
|
| 420 |
+
print(f"[DEBUG] 🎨 Valid candidates: {len(valid_candidates)}")
|
| 421 |
+
|
| 422 |
+
image = export_pose_as_image(
|
| 423 |
+
export_data,
|
| 424 |
+
canvas_size=(canvas_width, canvas_height),
|
| 425 |
+
enable_hands=draw_hand,
|
| 426 |
+
enable_face=draw_face
|
| 427 |
+
)
|
| 428 |
+
if image is None:
|
| 429 |
+
print(f"[DEBUG] ❌ 画像生成失敗 - export_pose_as_image returned None")
|
| 430 |
+
return gr.update(visible=False)
|
| 431 |
+
|
| 432 |
+
# タイムスタンプ付きファイル名でテンポラリファイルに保存
|
| 433 |
+
filename = get_timestamp_filename("dwpose_edit", "png")
|
| 434 |
+
temp_path = os.path.join(tempfile.gettempdir(), filename)
|
| 435 |
+
image.save(temp_path)
|
| 436 |
+
|
| 437 |
+
print(f"[DEBUG] ✅ ファイル準備完了: {temp_path}")
|
| 438 |
+
notify_success(f"画像をエクスポートしました: {filename}")
|
| 439 |
+
|
| 440 |
+
# 🎯 Button + File方式でファイルを表示
|
| 441 |
+
return gr.update(value=temp_path, visible=True)
|
| 442 |
+
|
| 443 |
+
except Exception as e:
|
| 444 |
+
print(f"[DEBUG] ❌ エラー: {e}")
|
| 445 |
+
notify_error(f"画像エクスポートエラー: {str(e)}")
|
| 446 |
+
return gr.update(visible=False)
|
| 447 |
|
| 448 |
def export_json(pose_data):
|
| 449 |
+
"""ポーズJSONをエクスポート(Button + File方式)(refs互換・グローバル管理)"""
|
| 450 |
+
global _current_pose_data
|
| 451 |
+
|
| 452 |
+
# グローバル変数のデータを優先使用(最新の編集データ)
|
| 453 |
+
export_data = _current_pose_data if _current_pose_data is not None else pose_data
|
| 454 |
+
|
| 455 |
+
print(f"[DEBUG] 📥 JSONダウンロードボタンクリック")
|
| 456 |
+
print(f"[DEBUG] 📊 グローバルデータ: {bool(_current_pose_data)}, 引数データ: {bool(pose_data)}")
|
| 457 |
+
print(f"[DEBUG] 📊 使用データ: {bool(export_data)}")
|
| 458 |
+
|
| 459 |
+
if not export_data:
|
| 460 |
notify_error("エクスポートするポーズデータがありません")
|
| 461 |
+
print(f"[DEBUG] ❌ データなし")
|
| 462 |
+
return gr.update(visible=False)
|
| 463 |
|
| 464 |
+
try:
|
| 465 |
+
json_str = export_pose_as_json(export_data)
|
| 466 |
+
if not json_str:
|
| 467 |
+
print(f"[DEBUG] ❌ JSON生成失敗")
|
| 468 |
+
return gr.update(visible=False)
|
| 469 |
+
|
| 470 |
+
# タイムスタンプ付きファイル名でテンポラリファイルに保存
|
| 471 |
+
filename = get_timestamp_filename("dwpose_data", "json")
|
| 472 |
+
temp_path = os.path.join(tempfile.gettempdir(), filename)
|
| 473 |
+
|
| 474 |
+
with open(temp_path, 'w', encoding='utf-8') as f:
|
| 475 |
+
f.write(json_str)
|
| 476 |
+
|
| 477 |
+
print(f"[DEBUG] ✅ ファイル準備完了: {temp_path}")
|
| 478 |
+
notify_success(f"JSONをエクスポートしました: {filename}")
|
| 479 |
+
|
| 480 |
+
# 🎯 Button + File方式でファイルを表示
|
| 481 |
+
return gr.update(value=temp_path, visible=True)
|
| 482 |
+
|
| 483 |
+
except Exception as e:
|
| 484 |
+
print(f"[DEBUG] ❌ エラー: {e}")
|
| 485 |
+
notify_error(f"JSONエクスポートエラー: {str(e)}")
|
| 486 |
+
return gr.update(visible=False)
|
| 487 |
+
|
| 488 |
+
def on_js_pose_update(js_pose_str):
|
| 489 |
+
"""JavaScript側からのポーズデータ更新(refs互換・マルチフレーム管理)"""
|
| 490 |
+
global _current_poses, _current_frame_index, _is_updating
|
| 491 |
+
|
| 492 |
+
update_timestamp = int(time.time() * 1000)
|
| 493 |
+
print(f"[DEBUG] 🔄 JavaScript→Python データ転送開始: {len(js_pose_str) if js_pose_str else 0}文字, timestamp={update_timestamp}")
|
| 494 |
+
print(f"[DEBUG] 🔍 更新フラグ状態: _is_updating={_is_updating}")
|
| 495 |
+
print(f"[DEBUG] 🔍 グローバル状態: _current_poses={bool(_current_poses)}, _current_frame_index={_current_frame_index}")
|
| 496 |
+
|
| 497 |
+
# データの中身をちょっと確認
|
| 498 |
+
if js_pose_str:
|
| 499 |
+
try:
|
| 500 |
+
data_preview = json.loads(js_pose_str)
|
| 501 |
+
print(f"[DEBUG] 🔍 受信データ構造:", {
|
| 502 |
+
'hasPeople': 'people' in data_preview,
|
| 503 |
+
'peopleCount': len(data_preview.get('people', [])),
|
| 504 |
+
'hasHandLeft': bool(data_preview.get('people', [{}])[0].get('hand_left_keypoints_2d')) if data_preview.get('people') else False,
|
| 505 |
+
'hasHandRight': bool(data_preview.get('people', [{}])[0].get('hand_right_keypoints_2d')) if data_preview.get('people') else False,
|
| 506 |
+
'hasFace': bool(data_preview.get('people', [{}])[0].get('face_keypoints_2d')) if data_preview.get('people') else False
|
| 507 |
+
})
|
| 508 |
+
except:
|
| 509 |
+
print(f"[DEBUG] 🔍 受信データパース失敗")
|
| 510 |
+
|
| 511 |
+
# Issue 038: 処理中フラグチェック(refs issue043準拠)
|
| 512 |
+
if _is_updating:
|
| 513 |
+
print(f"[DEBUG] ⚠️ データ更新処理中のため、新しい要求をスキップ (timestamp={update_timestamp})")
|
| 514 |
+
return gr.update(), "" # 現在の状態を維持
|
| 515 |
+
|
| 516 |
+
if not js_pose_str or js_pose_str.strip() == "":
|
| 517 |
+
return gr.update(), "" # pose_data更新なし、テキストボックスクリア
|
| 518 |
+
|
| 519 |
+
# 処理開始フラグを立てる
|
| 520 |
+
_is_updating = True
|
| 521 |
+
print(f"[DEBUG] 🔒 データ更新フラグ設定: _is_updating={_is_updating} (timestamp={update_timestamp})")
|
| 522 |
+
|
| 523 |
+
try:
|
| 524 |
+
# JSON文字列をパース
|
| 525 |
+
canvas_data = json.loads(js_pose_str)
|
| 526 |
+
print(f"[DEBUG] 🎨 Canvas JSON解析成功: keys={list(canvas_data.keys())}")
|
| 527 |
+
|
| 528 |
+
# refs互換:タイムスタンプを除去
|
| 529 |
+
if '_t' in canvas_data:
|
| 530 |
+
del canvas_data['_t']
|
| 531 |
+
print(f"[DEBUG] 🔄 タイムスタンプ除去完了")
|
| 532 |
+
|
| 533 |
+
# peopleフォーマットからpose_dataを抽出(refs互換方式)
|
| 534 |
+
if 'people' in canvas_data and canvas_data['people']:
|
| 535 |
+
pose_data = canvas_data['people'][0]
|
| 536 |
+
print(f"[DEBUG] 🎯 People形式データ抽出成功")
|
| 537 |
+
|
| 538 |
+
# _current_posesの初期化(初回のみ)
|
| 539 |
+
if _current_poses is None:
|
| 540 |
+
_current_poses = [{
|
| 541 |
+
'people': [pose_data],
|
| 542 |
+
'metadata': {
|
| 543 |
+
'resolution': canvas_data.get('resolution', [512, 512])
|
| 544 |
+
}
|
| 545 |
+
}]
|
| 546 |
+
print(f"[DEBUG] 🚀 _current_poses初期化完了")
|
| 547 |
+
else:
|
| 548 |
+
# 現在のフレームのpeopleデータを更新(refs互換)
|
| 549 |
+
if 0 <= _current_frame_index < len(_current_poses):
|
| 550 |
+
_current_poses[_current_frame_index]['people'] = [pose_data]
|
| 551 |
+
print(f"[DEBUG] 🎯 フレーム{_current_frame_index}のpeopleデータ更新完了")
|
| 552 |
+
else:
|
| 553 |
+
# フレームが範囲外の場合は追加
|
| 554 |
+
_current_poses.append({
|
| 555 |
+
'people': [pose_data],
|
| 556 |
+
'metadata': {
|
| 557 |
+
'resolution': canvas_data.get('resolution', [512, 512])
|
| 558 |
+
}
|
| 559 |
+
})
|
| 560 |
+
print(f"[DEBUG] 🎯 新フレーム追加完了")
|
| 561 |
+
|
| 562 |
+
# 表示用に現在のフレームデータを構築(既存UI互換)
|
| 563 |
+
current_frame_data = _current_poses[_current_frame_index]
|
| 564 |
+
display_data = {
|
| 565 |
+
'bodies': {'candidate': [], 'subset': []},
|
| 566 |
+
'hands': [],
|
| 567 |
+
'faces': [],
|
| 568 |
+
'resolution': current_frame_data['metadata'].get('resolution', [512, 512])
|
| 569 |
+
}
|
| 570 |
+
|
| 571 |
+
# people形式からbodies.candidate形式に変換(表示用)
|
| 572 |
+
if current_frame_data['people'] and current_frame_data['people'][0]:
|
| 573 |
+
person = current_frame_data['people'][0]
|
| 574 |
+
|
| 575 |
+
# pose_keypoints_2d をcandidate形式に変換
|
| 576 |
+
if 'pose_keypoints_2d' in person:
|
| 577 |
+
pose_keypoints = person['pose_keypoints_2d']
|
| 578 |
+
print(f"[DEBUG] 🔄 pose_keypoints length: {len(pose_keypoints)}")
|
| 579 |
+
|
| 580 |
+
if len(pose_keypoints) >= 3:
|
| 581 |
+
for i in range(0, len(pose_keypoints), 3):
|
| 582 |
+
if i + 2 < len(pose_keypoints):
|
| 583 |
+
x = pose_keypoints[i]
|
| 584 |
+
y = pose_keypoints[i + 1]
|
| 585 |
+
conf = pose_keypoints[i + 2]
|
| 586 |
+
display_data['bodies']['candidate'].append([x, y, conf, 0])
|
| 587 |
+
|
| 588 |
+
# 手と顔データ(表示用)
|
| 589 |
+
left_hand = person.get('hand_left_keypoints_2d', [])
|
| 590 |
+
right_hand = person.get('hand_right_keypoints_2d', [])
|
| 591 |
+
face_data = person.get('face_keypoints_2d', [])
|
| 592 |
+
|
| 593 |
+
display_data['hands'] = [left_hand, right_hand]
|
| 594 |
+
display_data['faces'] = [face_data] if face_data else []
|
| 595 |
+
|
| 596 |
+
print(f"[DEBUG] 🫳 Hand data: left={len(left_hand)}, right={len(right_hand)}")
|
| 597 |
+
print(f"[DEBUG] 😊 Face data: {len(face_data) if face_data else 0}")
|
| 598 |
+
|
| 599 |
+
print(f"[DEBUG] ✅ refs互換データ更新完了: frames={len(_current_poses)}")
|
| 600 |
+
print(f"[DEBUG] 📊 表示用データ: candidates={len(display_data['bodies']['candidate'])}")
|
| 601 |
+
|
| 602 |
+
# pose_dataコンポーネントを更新、テキストボックスをクリア
|
| 603 |
+
return display_data, ""
|
| 604 |
+
|
| 605 |
+
except json.JSONDecodeError as e:
|
| 606 |
+
print(f"[DEBUG] ❌ JavaScript→Python JSONパースエラー: {e}")
|
| 607 |
+
return gr.update(), "" # エラー時は更新せず、テキストボックスクリア
|
| 608 |
+
except Exception as e:
|
| 609 |
+
print(f"[DEBUG] ❌ JavaScript→Python エラー: {e}")
|
| 610 |
+
return gr.update(), ""
|
| 611 |
+
finally:
|
| 612 |
+
# Issue 038: 処理完了後は必ずフラグを解除(refs issue043準拠)
|
| 613 |
+
old_flag = _is_updating
|
| 614 |
+
_is_updating = False
|
| 615 |
+
print(f"[DEBUG] 🔓 データ更新処理完了 - フラグ解除: {old_flag} → {_is_updating} (timestamp={update_timestamp})")
|
| 616 |
+
|
| 617 |
+
def on_json_upload(file):
|
| 618 |
+
"""JSONファイルアップロード時の処理(refs互換・グローバル管理)"""
|
| 619 |
+
global _current_pose_data
|
| 620 |
+
|
| 621 |
+
if file is None:
|
| 622 |
+
return None, {}
|
| 623 |
+
|
| 624 |
+
try:
|
| 625 |
+
# ファイルを読み込む
|
| 626 |
+
with open(file.name, 'r', encoding='utf-8') as f:
|
| 627 |
+
json_content = f.read()
|
| 628 |
+
|
| 629 |
+
# JSONをパース
|
| 630 |
+
loaded_data = json.loads(json_content)
|
| 631 |
+
|
| 632 |
+
# データ検証
|
| 633 |
+
if not validate_pose_json(loaded_data):
|
| 634 |
+
notify_error("無効なポーズデータフォーマットです")
|
| 635 |
+
return None, {}
|
| 636 |
+
|
| 637 |
+
# メタデータを除去(存在する場合)
|
| 638 |
+
if 'metadata' in loaded_data:
|
| 639 |
+
del loaded_data['metadata']
|
| 640 |
+
|
| 641 |
+
# グローバル変数に保存(refs互換)
|
| 642 |
+
_current_pose_data = loaded_data.copy()
|
| 643 |
+
print(f"[DEBUG] ✅ グローバル変数更新完了(JSON読み込み)")
|
| 644 |
+
|
| 645 |
+
notify_success("JSONファイルを読み込みました")
|
| 646 |
+
return loaded_data, loaded_data
|
| 647 |
+
|
| 648 |
+
except json.JSONDecodeError as e:
|
| 649 |
+
notify_error(f"JSONパースエラー: {str(e)}")
|
| 650 |
+
return None, {}
|
| 651 |
+
except Exception as e:
|
| 652 |
+
notify_error(f"ファイル読み込みエラー: {str(e)}")
|
| 653 |
+
return None, {}
|
| 654 |
+
|
| 655 |
+
def validate_pose_json(data):
|
| 656 |
+
"""ポーズJSONデータの検証"""
|
| 657 |
+
if not isinstance(data, dict):
|
| 658 |
+
return False
|
| 659 |
+
|
| 660 |
+
# 最低限必要なキーをチェック
|
| 661 |
+
required_keys = ['bodies']
|
| 662 |
+
for key in required_keys:
|
| 663 |
+
if key not in data:
|
| 664 |
+
return False
|
| 665 |
+
|
| 666 |
+
# bodiesの構造をチェック
|
| 667 |
+
if 'bodies' in data:
|
| 668 |
+
bodies = data['bodies']
|
| 669 |
+
if not isinstance(bodies, dict):
|
| 670 |
+
return False
|
| 671 |
+
if 'candidate' not in bodies and 'subset' not in bodies:
|
| 672 |
+
return False
|
| 673 |
+
|
| 674 |
+
return True
|
| 675 |
|
| 676 |
# 隠しコンポーネント(JavaScript実行用)
|
| 677 |
js_executor = gr.HTML(visible=False, elem_id="js_executor")
|
|
|
|
| 680 |
input_image.change(
|
| 681 |
fn=on_image_upload,
|
| 682 |
inputs=[input_image],
|
| 683 |
+
outputs=[output_json, pose_data, js_executor]
|
| 684 |
)
|
| 685 |
|
| 686 |
# pose_data変更時にCanvas更新(重要!)- 無限ループ防止
|
|
|
|
| 724 |
outputs=[output_json, pose_data]
|
| 725 |
)
|
| 726 |
|
| 727 |
+
# エクスポートイベント (refs互換DownloadButton方式)
|
| 728 |
download_image_btn.click(
|
| 729 |
fn=export_image,
|
| 730 |
+
inputs=[pose_data, draw_hand, draw_face],
|
| 731 |
+
outputs=[download_image_file]
|
| 732 |
)
|
| 733 |
|
| 734 |
download_json_btn.click(
|
| 735 |
fn=export_json,
|
| 736 |
inputs=[pose_data],
|
| 737 |
+
outputs=[download_json_file]
|
| 738 |
+
)
|
| 739 |
+
|
| 740 |
+
# JavaScript→Python データ転送イベント
|
| 741 |
+
js_pose_update.change(
|
| 742 |
+
fn=on_js_pose_update,
|
| 743 |
+
inputs=[js_pose_update],
|
| 744 |
+
outputs=[pose_data, js_pose_update] # pose_data更新 + テキストボックスクリア
|
| 745 |
+
)
|
| 746 |
+
|
| 747 |
+
# JSONファイルアップロードイベント
|
| 748 |
+
json_upload.change(
|
| 749 |
+
fn=on_json_upload,
|
| 750 |
+
inputs=[json_upload],
|
| 751 |
+
outputs=[output_json, pose_data]
|
| 752 |
)
|
| 753 |
|
| 754 |
return demo
|
issues/029_手顔表示リアルタイム反映修正.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Issue 029: 手顔表示リアルタイム反映修正 🎨🔧
|
| 2 |
+
|
| 3 |
+
## 📋 問題概要
|
| 4 |
+
|
| 5 |
+
手を表示・顔を表示のチェックボックス変更が、Canvasにリアルタイムで反映されない。オフにした時は即座に手や顔が非表示になるべき。
|
| 6 |
+
|
| 7 |
+
## 🎯 解決目標
|
| 8 |
+
|
| 9 |
+
1. **リアルタイム更新**: チェックボックス変更時に即座にCanvas更新
|
| 10 |
+
2. **手表示制御**: 手を表示のオン/オフがすぐに反映
|
| 11 |
+
3. **顔表示制御**: 顔を表示のオン/オフがすぐに反映
|
| 12 |
+
4. **スムーズな操作**: ユーザーがチェックボックスを変更したらすぐに結果が見える
|
| 13 |
+
|
| 14 |
+
## 🔧 実装内容
|
| 15 |
+
|
| 16 |
+
### app.py の修正
|
| 17 |
+
- 表示設定変更イベントの強化
|
| 18 |
+
- JavaScript側への即座の設定反映
|
| 19 |
+
|
| 20 |
+
### static/pose_editor.js の修正
|
| 21 |
+
- updateDisplaySettings関数の改善
|
| 22 |
+
- drawPose関数での表示設定チェック強化
|
| 23 |
+
|
| 24 |
+
## ✅ 完了条件
|
| 25 |
+
|
| 26 |
+
- [ ] 手を表示チェックボックスのオン/オフが即座に反映される
|
| 27 |
+
- [ ] 顔を表示チェックボックスのオン/オフが即座に反映される
|
| 28 |
+
- [ ] チェックボックス変更後にCanvas再描画が実行される
|
| 29 |
+
- [ ] 表示設定がグローバル変数に正しく保存される
|
| 30 |
+
|
| 31 |
+
## 📝 参考資料
|
| 32 |
+
|
| 33 |
+
- refs/dwpose_modifier の表示設定実装
|
| 34 |
+
- Gradio イベントハンドリング
|
| 35 |
+
|
| 36 |
+
## 🏷️ タグ
|
| 37 |
+
|
| 38 |
+
`bug` `ui` `realtime` `display-settings`
|
issues/030_JSONダウンロードボタン表示修正.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Issue 030: JSONダウンロードボタン表示修正 📁🔧
|
| 2 |
+
|
| 3 |
+
## 📋 問題概要
|
| 4 |
+
|
| 5 |
+
JSONをダウンロードボタンを押すと、「画像をダウンロード」というメッセージが表示される。正しくは「JSONをダウンロード」メッセージが表示されるべき。
|
| 6 |
+
|
| 7 |
+
## 🎯 解決目標
|
| 8 |
+
|
| 9 |
+
1. **正しいメッセージ表示**: JSONダウンロード時に適切なメッセージ
|
| 10 |
+
2. **通知の統一**: 画像とJSONで異なる通知メッセージ
|
| 11 |
+
3. **ユーザー体験改善**: 何をダウンロードしたかが分かりやすい
|
| 12 |
+
|
| 13 |
+
## 🔧 実装内容
|
| 14 |
+
|
| 15 |
+
### app.py の修正
|
| 16 |
+
- export_json関数の通知メッセージ修正
|
| 17 |
+
- notify_success呼び出しの確認
|
| 18 |
+
|
| 19 |
+
### utils/export_utils.py の修正
|
| 20 |
+
- export_pose_as_json関数の通知メッセージ確認
|
| 21 |
+
|
| 22 |
+
## ✅ 完了条件
|
| 23 |
+
|
| 24 |
+
- [ ] JSONダウンロード時に「JSONをダウンロード」メッセージが表示される
|
| 25 |
+
- [ ] 画像ダウンロード時は「画像をダウンロード」メッセージのまま
|
| 26 |
+
- [ ] 通知メッセージが処理内容と一致している
|
| 27 |
+
|
| 28 |
+
## 📝 参考資料
|
| 29 |
+
|
| 30 |
+
- utils/notifications.py の通知システム
|
| 31 |
+
- Gradio 通知機能
|
| 32 |
+
|
| 33 |
+
## 🏷️ タグ
|
| 34 |
+
|
| 35 |
+
`bug` `ui` `notification` `download`
|
issues/031_JSONダウンロード保存先修正.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Issue 031: JSONダウンロード保存先修正 💾🔧
|
| 2 |
+
|
| 3 |
+
## 📋 問題概要
|
| 4 |
+
|
| 5 |
+
JSONダウンロードボタンを押してもブラウザのデフォルトダウンロードフォルダにファイルが保存されない。適切なファイルダウンロード機能が動作していない。
|
| 6 |
+
|
| 7 |
+
## 🎯 解決目標
|
| 8 |
+
|
| 9 |
+
1. **ファイルダウンロード機能**: ブラウザのダウンロードフォルダに保存
|
| 10 |
+
2. **適切なMIMEタイプ**: JSONファイルとして認識される
|
| 11 |
+
3. **ファイル名の保持**: タイムスタンプ付きファイル名でダウンロード
|
| 12 |
+
4. **Gradio File機能**: gr.Fileコンポーネントの正しい使用
|
| 13 |
+
|
| 14 |
+
## 🔧 実装内容
|
| 15 |
+
|
| 16 |
+
### app.py の修正
|
| 17 |
+
- gr.Fileコンポーネントの設定確認
|
| 18 |
+
- ダウンロードイベントハンドラーの修正
|
| 19 |
+
- ファイルパス返却方式の見直し
|
| 20 |
+
|
| 21 |
+
### ブラウザダウンロード機能
|
| 22 |
+
- Content-Dispositionヘッダーの設定
|
| 23 |
+
- ファイルダウンロードトリガーの実装
|
| 24 |
+
|
| 25 |
+
## ✅完了条件
|
| 26 |
+
|
| 27 |
+
- [ ] JSONファイルがブラウザのダウンロードフォルダに保存される
|
| 28 |
+
- [ ] ファイル名が正しく設定される(dwpose_data_YYYYMMDD_HHMMSS.json)
|
| 29 |
+
- [ ] ダウンロード時にブラウザの保存ダイアログが表示される
|
| 30 |
+
- [ ] JSON形式として正しく認識される
|
| 31 |
+
|
| 32 |
+
## 📝 参考資料
|
| 33 |
+
|
| 34 |
+
- Gradio File download 機能
|
| 35 |
+
- ブラウザファイルダウンロード仕様
|
| 36 |
+
- refs/dwpose_modifier のダウンロード実装
|
| 37 |
+
|
| 38 |
+
## 🏷️ タグ
|
| 39 |
+
|
| 40 |
+
`bug` `download` `file-handling` `browser`
|
issues/032_画像ダウンロードボタン動作修正.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Issue 032: 画像ダウンロードボタン動作修正 🖼️🔧
|
| 2 |
+
|
| 3 |
+
## 📋 問題概要
|
| 4 |
+
|
| 5 |
+
画像をダウンロードボタンが効かない。ボタンを押してもファイルダウンロードが実行されない状態。
|
| 6 |
+
|
| 7 |
+
## 🎯 解決目標
|
| 8 |
+
|
| 9 |
+
1. **ボタン動作修復**: クリック時にダウンロード処理が実行される
|
| 10 |
+
2. **画像ファイル生成**: ポーズデータから画像を正しく生成
|
| 11 |
+
3. **ファイルダウンロード**: ブラウザでファイル保存が実行される
|
| 12 |
+
4. **エラーハンドリング**: 失敗時の適切なエラー表示
|
| 13 |
+
|
| 14 |
+
## 🔧 実装内容
|
| 15 |
+
|
| 16 |
+
### app.py の修正
|
| 17 |
+
- export_image関数のデバッグ
|
| 18 |
+
- download_image_btn.clickイベントの確認
|
| 19 |
+
- gr.Fileコンポーネントへの出力確認
|
| 20 |
+
|
| 21 |
+
### utils/export_utils.py の修正
|
| 22 |
+
- export_pose_as_image関数の動作確認
|
| 23 |
+
- 画像生成処理のデバッグ
|
| 24 |
+
|
| 25 |
+
### エラー調査
|
| 26 |
+
- コンソールエラーメッセージの確認
|
| 27 |
+
- 処理フローの検証
|
| 28 |
+
|
| 29 |
+
## ✅ 完了条件
|
| 30 |
+
|
| 31 |
+
- [ ] 画像ダウンロードボタンをクリックするとファイルがダウンロードされる
|
| 32 |
+
- [ ] 手・顔の表示設定が画像に正しく反映される
|
| 33 |
+
- [ ] ファイル名が適切に設定される(dwpose_edit_YYYYMMDD_HHMMSS.png)
|
| 34 |
+
- [ ] エラー時に適切な通知が表示される
|
| 35 |
+
|
| 36 |
+
## 📝 参考資料
|
| 37 |
+
|
| 38 |
+
- Gradio Button and File component
|
| 39 |
+
- PIL Image処理
|
| 40 |
+
- 一時ファイル生成とダウンロード
|
| 41 |
+
|
| 42 |
+
## 🏷️ タグ
|
| 43 |
+
|
| 44 |
+
`bug` `download` `image` `button-action`
|
issues/033_詳細モードキーポイント直接編集実装.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Issue 033: 詳細モードキーポイント直接編集実装 ✏️💫
|
| 2 |
+
|
| 3 |
+
## 📋 問題概要
|
| 4 |
+
|
| 5 |
+
詳細モードに切り替えても、手や顔のキーポイントを直接ドラッグして編集できない。矩形の表示もラジオボタン変更にリアルタイムで反映されていない。
|
| 6 |
+
|
| 7 |
+
## 🎯 解決目標
|
| 8 |
+
|
| 9 |
+
1. **詳細モード直接編集**: キーポイントを個別にドラッグして編集可能
|
| 10 |
+
2. **リアルタイム切り替え**: 簡易モード↔詳細モード即座に反映
|
| 11 |
+
3. **矩形表示制御**: 簡易モードでのみ矩形表示、詳細モードでは非表示
|
| 12 |
+
4. **キーポイント操作**: 手・顔の個別キーポイント選択とドラッグ
|
| 13 |
+
|
| 14 |
+
## 🔧 実装内容
|
| 15 |
+
|
| 16 |
+
### static/pose_editor.js の修正
|
| 17 |
+
- 詳細モードでのマウスイベント処理追加
|
| 18 |
+
- キーポイント直接選択機能の実装
|
| 19 |
+
- モード切り替え時の表示更新
|
| 20 |
+
|
| 21 |
+
### キーポイント編集機能
|
| 22 |
+
```javascript
|
| 23 |
+
// 詳細モードでのキーポイント選択
|
| 24 |
+
function findNearestKeypointInDetailMode(mouseX, mouseY) {
|
| 25 |
+
// 手・顔の個別キーポイントを検索
|
| 26 |
+
// ドラッグ可能なキーポイントを特定
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
// 詳細モードでのドラッグ処理
|
| 30 |
+
function handleDetailModeMouseMove(event) {
|
| 31 |
+
// 選択されたキーポイントを移動
|
| 32 |
+
// リアルタイム更新
|
| 33 |
+
}
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
### モード切り替え改善
|
| 37 |
+
- updateDisplaySettings関数の強化
|
| 38 |
+
- 矩形の即座非表示・表示
|
| 39 |
+
|
| 40 |
+
## ✅ 完了条件
|
| 41 |
+
|
| 42 |
+
- [ ] 詳細モードで手のキーポイントを個別ドラッグできる
|
| 43 |
+
- [ ] 詳細モードで顔のキーポイントを個別ドラッグできる
|
| 44 |
+
- [ ] ラジオボタン変更時に即座に矩形表示が切り替わる
|
| 45 |
+
- [ ] 詳細モードでは矩形が表示されない
|
| 46 |
+
- [ ] 簡易モードでは矩形が表示される
|
| 47 |
+
- [ ] キーポイント選択時の視覚的フィードバックがある
|
| 48 |
+
|
| 49 |
+
## 📝 参考資料
|
| 50 |
+
|
| 51 |
+
- refs/dwpose_modifier の詳細モード実装
|
| 52 |
+
- Issue #023: 詳細モード矩形非表示修正
|
| 53 |
+
- DWPoseキーポイント構造
|
| 54 |
+
|
| 55 |
+
## 🏷️ タグ
|
| 56 |
+
|
| 57 |
+
`bug` `editing` `keypoint` `detail-mode` `interaction`
|
issues/034_画像ダウンロード機能修正.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Issue #034: 画像ダウンロード機能修正 🖼️
|
| 2 |
+
|
| 3 |
+
## 📋 問題の概要
|
| 4 |
+
|
| 5 |
+
画像ダウンロードボタンをクリックした際に、`export_pose_as_image`関数がNoneを返して画像生成に失敗している。
|
| 6 |
+
|
| 7 |
+
## 🐛 再現手順
|
| 8 |
+
|
| 9 |
+
1. アプリを起動
|
| 10 |
+
2. 画像をアップロードしてポーズ検出を実行
|
| 11 |
+
3. 「📥 画像をダウンロード」ボタンをクリック
|
| 12 |
+
4. コンソールに「[DEBUG] ❌ 画像生成失敗」が表示される
|
| 13 |
+
|
| 14 |
+
## 🔍 デバッグ情報
|
| 15 |
+
|
| 16 |
+
```
|
| 17 |
+
[DEBUG] 🖼️ 画像ダウンロードボタンクリック - データ: True, 手: True, 顔: True
|
| 18 |
+
[DEBUG] ❌ 画像生成失敗
|
| 19 |
+
```
|
| 20 |
+
|
| 21 |
+
- ポーズデータは存在している(データ: True)
|
| 22 |
+
- 手と顔の描画設定も正しく渡されている
|
| 23 |
+
- しかし`export_pose_as_image`関数がNoneを返している
|
| 24 |
+
|
| 25 |
+
## 🎯 原因分析
|
| 26 |
+
|
| 27 |
+
### 推測される原因
|
| 28 |
+
1. **ポーズデータ形式の問題**: DWPoseデータの形式が`export_pose_as_image`関数の期待する形式と異なる
|
| 29 |
+
2. **描画関数の実装不備**: `utils/export_utils.py`の描画ロジックに問題がある
|
| 30 |
+
3. **座標系の問題**: キャンバス座標とエクスポート座標の変換に問題がある
|
| 31 |
+
|
| 32 |
+
### 確認すべき箇所
|
| 33 |
+
- `utils/export_utils.py`の`export_pose_as_image`関数
|
| 34 |
+
- ポーズデータの実際の構造
|
| 35 |
+
- 描画関数(`draw_body_on_image`, `draw_hands_on_image`, `draw_faces_on_image`)
|
| 36 |
+
|
| 37 |
+
## 🔧 解決アプローチ
|
| 38 |
+
|
| 39 |
+
### Step 1: ポーズデータ構造の確認
|
| 40 |
+
- 実際のポーズデータの内容をログ出力
|
| 41 |
+
- DWPose形式の確認
|
| 42 |
+
|
| 43 |
+
### Step 2: refs実装の参照
|
| 44 |
+
- `refs/dwpose_modifier`の描画実装を確認
|
| 45 |
+
- 正しい描画パターンの適用
|
| 46 |
+
|
| 47 |
+
### Step 3: エラーハンドリング強化
|
| 48 |
+
- より詳細なエラーログの追加
|
| 49 |
+
- 各段階での失敗原因の特定
|
| 50 |
+
|
| 51 |
+
## 💡 修正計画
|
| 52 |
+
|
| 53 |
+
1. **ログ強化**: ポーズデータの詳細な内容確認
|
| 54 |
+
2. **refs参照**: 正しい描画実装パターンの適用
|
| 55 |
+
3. **段階的テスト**: 各描画関数の個別テスト
|
| 56 |
+
4. **形式変換**: 必要に応じてポーズデータ形式の変換実装
|
| 57 |
+
|
| 58 |
+
## ⏰ 優先度
|
| 59 |
+
|
| 60 |
+
**High** - ダウンロード機能は基本機能として重要
|
| 61 |
+
|
| 62 |
+
## 📝 関連Issue
|
| 63 |
+
|
| 64 |
+
- Issue #035: JSONダウンロード機能修正
|
| 65 |
+
- Issue #036: Canvas編集データ反映修正
|
| 66 |
+
|
| 67 |
+
## ✅ 完了条件
|
| 68 |
+
|
| 69 |
+
- [x] 画像ダウンロードボタンが正常に動作する
|
| 70 |
+
- [x] エクスポートされた画像にポーズが正しく描画される
|
| 71 |
+
- [x] 手と顔の表示設定が正しく反映される
|
| 72 |
+
- [x] エラー時の適切な通知表示
|
| 73 |
+
|
| 74 |
+
## 🔧 修正内容
|
| 75 |
+
|
| 76 |
+
### 主要修正
|
| 77 |
+
1. **描画ロジック完全書き直し**: refsに合わせてpeopleベースの描画に変更
|
| 78 |
+
2. **色空間修正**: BGR→RGB変換で正しい色表示
|
| 79 |
+
3. **背景色修正**: 白→黒に変更
|
| 80 |
+
4. **解像度対応**: ポーズデータの解像度情報に基づく画像サイズ
|
| 81 |
+
|
| 82 |
+
### コード変更
|
| 83 |
+
- `utils/export_utils.py`: `draw_body_on_image`関数の完全書き直し
|
| 84 |
+
- `app.py`: 解像度準拠の画像エクスポート実装
|
| 85 |
+
|
| 86 |
+
---
|
| 87 |
+
|
| 88 |
+
**作成日**: 2025-01-11
|
| 89 |
+
**完了日**: 2025-01-11
|
| 90 |
+
**担当**: Claude Code
|
| 91 |
+
**ステータス**: ✅ Completed
|
issues/035_JSONダウンロード機能修正.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Issue #035: JSONダウンロード機能修正 📥
|
| 2 |
+
|
| 3 |
+
## 📋 問題の概要
|
| 4 |
+
|
| 5 |
+
DownloadButton修正後にJSONダウンロード機能が動作しなくなった。以前は2クリック必要だったが、現在は全く動作しない状況。
|
| 6 |
+
|
| 7 |
+
## 🐛 再現手順
|
| 8 |
+
|
| 9 |
+
1. アプリを起動
|
| 10 |
+
2. 画像をアップロードしてポーズ検出を実行
|
| 11 |
+
3. 「📥 JSONをダウンロード」ボタンをクリック
|
| 12 |
+
4. ダウンロードが開始されない
|
| 13 |
+
|
| 14 |
+
## 🔍 現在の実装状況
|
| 15 |
+
|
| 16 |
+
### 修正内容
|
| 17 |
+
- `gr.DownloadButton`の使用(refs互換)
|
| 18 |
+
- `export_json`関数の戻り値を`gr.update(value=temp_path, visible=True)`に変更
|
| 19 |
+
- イベントハンドラーの出力を`outputs=[download_json_btn]`に変更
|
| 20 |
+
|
| 21 |
+
### 問題の可能性
|
| 22 |
+
1. **DownloadButtonの使用方法**: Gradioの最新バージョンでの正しい使用法
|
| 23 |
+
2. **ファイルパスの問題**: 一時ファイルパスが正しく渡されていない
|
| 24 |
+
3. **イベント処理の問題**: クリックイベントの処理に問題がある
|
| 25 |
+
|
| 26 |
+
## 🎯 原因分析
|
| 27 |
+
|
| 28 |
+
### 確認すべき箇所
|
| 29 |
+
- DownloadButtonのGradioドキュメント
|
| 30 |
+
- refs実装での実際の動作確認
|
| 31 |
+
- Gradioのバージョン互換性
|
| 32 |
+
- ブラウザのネットワークタブでのリクエスト確認
|
| 33 |
+
|
| 34 |
+
## 🔧 解決アプローチ
|
| 35 |
+
|
| 36 |
+
### Step 1: refs実装の動作確認
|
| 37 |
+
- `refs/dwpose_modifier`でDownloadButtonが実際に動作するか確認
|
| 38 |
+
- 同じGradioバージョンでの動作パターン確認
|
| 39 |
+
|
| 40 |
+
### Step 2: デバッグログ強化
|
| 41 |
+
- クリックイベントの発火確認
|
| 42 |
+
- ファイル生成の成功確認
|
| 43 |
+
- gr.updateの戻り値確認
|
| 44 |
+
|
| 45 |
+
### Step 3: 代替実装の検討
|
| 46 |
+
- Button + File組み合わせでの実装に戻す
|
| 47 |
+
- 他のDownloadButton実装パターンの検討
|
| 48 |
+
|
| 49 |
+
## 💡 修正計画
|
| 50 |
+
|
| 51 |
+
1. **refs動作確認**: 実際のrefsでの動作テスト
|
| 52 |
+
2. **ログ追加**: 詳細なデバッグ情報の出力
|
| 53 |
+
3. **段階的修正**: 機能する最小実装から構築
|
| 54 |
+
4. **ユーザビリティ向上**: 1クリックダウンロードの実現
|
| 55 |
+
|
| 56 |
+
## ⏰ 優先度
|
| 57 |
+
|
| 58 |
+
**High** - 基本的なエクスポート機能として重要
|
| 59 |
+
|
| 60 |
+
## 📝 関連Issue
|
| 61 |
+
|
| 62 |
+
- Issue #034: 画像ダウンロード機能修正
|
| 63 |
+
- Issue #036: Canvas編集データ反映修正
|
| 64 |
+
|
| 65 |
+
## ✅ 完了条件
|
| 66 |
+
|
| 67 |
+
- [ ] JSONダウンロードボタンが1クリックで動作する
|
| 68 |
+
- [ ] 正しいファイル名でダウンロードされる
|
| 69 |
+
- [ ] 適切な通知メッセージが表示される
|
| 70 |
+
- [ ] ブラウザのデフォルトダウンロードフォルダに保存される
|
| 71 |
+
|
| 72 |
+
---
|
| 73 |
+
|
| 74 |
+
**作成日**: 2025-01-11
|
| 75 |
+
**担当**: Claude Code
|
| 76 |
+
**ステータス**: 🔧 In Progress
|
issues/036_Canvas編集データ反映修正.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Issue #036: Canvas編集データ反映修正 🎨
|
| 2 |
+
|
| 3 |
+
## 📋 問題の概要
|
| 4 |
+
|
| 5 |
+
Canvas上でポーズを編集してもその変更がJSONダウンロードに反映されない。JavaScript側での編集がPython側のポーズデータに同期されていない。
|
| 6 |
+
|
| 7 |
+
## 🐛 再現手順
|
| 8 |
+
|
| 9 |
+
1. アプリを起動
|
| 10 |
+
2. 画像をアップロードしてポーズ検出を実行
|
| 11 |
+
3. Canvas上でキーポイントをドラッグして編集
|
| 12 |
+
4. 「📥 JSONをダウンロード」ボタンをクリック
|
| 13 |
+
5. ダウンロードされたJSONに編集内容が反映されていない
|
| 14 |
+
|
| 15 |
+
## 🔍 現在の実装状況
|
| 16 |
+
|
| 17 |
+
### JavaScript → Python データ同期
|
| 18 |
+
- `static/pose_editor.js`でポーズ編集実装済み
|
| 19 |
+
- `gradioCanvasUpdate`関数でデータ更新実装済み
|
| 20 |
+
- しかし、編集データがPython側の`pose_data`コンポーネントに反映されていない
|
| 21 |
+
|
| 22 |
+
### 確認すべき箇所
|
| 23 |
+
1. **JavaScript側**: 編集後のデータ送信処理
|
| 24 |
+
2. **Python側**: ポーズデータの受信・更新処理
|
| 25 |
+
3. **Gradio連携**: JavaScript → Python データ転送メカニズム
|
| 26 |
+
|
| 27 |
+
## 🎯 原因分析
|
| 28 |
+
|
| 29 |
+
### 推測される原因
|
| 30 |
+
1. **イベントハンドラーの問題**: Canvas編集イベントがGradioに正しく伝わっていない
|
| 31 |
+
2. **データ形式の問題**: JavaScript側のデータ形式とPython側の期待形式が異なる
|
| 32 |
+
3. **同期タイミングの問題**: データ更新のタイミングが合っていない
|
| 33 |
+
4. **コンポーネント参照の問題**: 正しいGradioコンポーネントが更新されていない
|
| 34 |
+
|
| 35 |
+
### 確認ポイント
|
| 36 |
+
- `pose_data.change`イベントの動作
|
| 37 |
+
- JavaScript → Gradio データ転送の実装
|
| 38 |
+
- ポーズデータの実際の更新状況
|
| 39 |
+
|
| 40 |
+
## 🔧 解決アプローチ
|
| 41 |
+
|
| 42 |
+
### Step 1: データフローの確認
|
| 43 |
+
- JavaScript側での編集データの確認
|
| 44 |
+
- Gradio側でのデータ受信確認
|
| 45 |
+
- `pose_data`コンポーネントの更新状況確認
|
| 46 |
+
|
| 47 |
+
### Step 2: refs実装の参照
|
| 48 |
+
- `refs/dwpose_modifier`での編集データ同期方法の確認
|
| 49 |
+
- 正しいデータ転送パターンの適用
|
| 50 |
+
|
| 51 |
+
### Step 3: デバッグ強化
|
| 52 |
+
- Canvas編集時の詳細ログ追加
|
| 53 |
+
- データ同期の各段階でのログ出力
|
| 54 |
+
|
| 55 |
+
## 💡 修正計画
|
| 56 |
+
|
| 57 |
+
1. **データ同期メカニズムの修正**: JavaScript → Gradio データ転送の改善
|
| 58 |
+
2. **イベントハンドラーの見直し**: 正しいデータ更新イベントの実装
|
| 59 |
+
3. **データ形式の統一**: JS側とPython側のデータ形式統一
|
| 60 |
+
4. **リアルタイム同期**: 編集と同時にデータ更新される仕組み構築
|
| 61 |
+
|
| 62 |
+
## ⏰ 優先度
|
| 63 |
+
|
| 64 |
+
**High** - ポーズ編集機能の根幹となる重要な機能
|
| 65 |
+
|
| 66 |
+
## 📝 関連Issue
|
| 67 |
+
|
| 68 |
+
- Issue #034: 画像ダウンロード機能修正
|
| 69 |
+
- Issue #035: JSONダウンロード機能修正
|
| 70 |
+
|
| 71 |
+
## ✅ 完了条件
|
| 72 |
+
|
| 73 |
+
- [x] Canvas上でのポーズ編集がリアルタイムでPythonデータに反映される
|
| 74 |
+
- [x] 編集後のJSONダウンロードに編集内容が正しく含まれる
|
| 75 |
+
- [x] 詳細モードでの個別キーポイント編集が正常に機能する
|
| 76 |
+
- [x] 簡易モードでの矩形編集が正常に機能する
|
| 77 |
+
|
| 78 |
+
## 🔧 修正内容
|
| 79 |
+
|
| 80 |
+
### 主要修正
|
| 81 |
+
1. **専用データ転送チャネル**: `js_pose_update`隠しテキストボックス追加
|
| 82 |
+
2. **Python側受信処理**: `on_js_pose_update`関数で確実なデータ転送
|
| 83 |
+
3. **JavaScript側送信改良**: 専用テキストボックスを使用した確実な送信
|
| 84 |
+
|
| 85 |
+
### コード変更
|
| 86 |
+
- `app.py`: `js_pose_update`コンポーネントと`on_js_pose_update`関数追加
|
| 87 |
+
- `static/pose_editor.js`: `sendPoseDataToGradio`関数の改良
|
| 88 |
+
|
| 89 |
+
---
|
| 90 |
+
|
| 91 |
+
**作成日**: 2025-01-11
|
| 92 |
+
**完了日**: 2025-01-11
|
| 93 |
+
**担当**: Claude Code
|
| 94 |
+
**ステータス**: ✅ Completed
|
issues/038_Canvas編集データ完全同期修正.md
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Issue #038: Canvas編集データ完全同期修正 🔄💥
|
| 2 |
+
|
| 3 |
+
## 📋 問題の概要
|
| 4 |
+
|
| 5 |
+
Canvas上でポーズを編集しても、その結果がJSONダウンロードや画像エクスポートに全く反映されない根本的な問題。JavaScript→Python間のデータ同期が完全に機能していない。
|
| 6 |
+
|
| 7 |
+
## 🐛 現在のエラー
|
| 8 |
+
|
| 9 |
+
```javascript
|
| 10 |
+
VM838:760 Uncaught ReferenceError: currentPoseData is not defined
|
| 11 |
+
at HTMLCanvasElement.handleMouseUp (<anonymous>:760:35)
|
| 12 |
+
```
|
| 13 |
+
|
| 14 |
+
`handleMouseUp`関数内で`currentPoseData`が未定義のエラーが発生。
|
| 15 |
+
|
| 16 |
+
## 🔍 根本的な問題
|
| 17 |
+
|
| 18 |
+
### 1. データ同期の失敗
|
| 19 |
+
- Canvas編集時のデータがPython側の`pose_data`コンポーネントに反映されない
|
| 20 |
+
- JavaScript側で編集した座標が保存時に消える
|
| 21 |
+
|
| 22 |
+
### 2. データ構造の不整合
|
| 23 |
+
- 現在の構造:`bodies.candidate`と`people.pose_keypoints_2d`の二重構造
|
| 24 |
+
- 編集は`bodies.candidate`で行うが、エクスポートは`people`形式を使用
|
| 25 |
+
- 同期処理が不完全
|
| 26 |
+
|
| 27 |
+
### 3. イベント処理の問題
|
| 28 |
+
- Gradioの`change`イベントが正しく発火しない
|
| 29 |
+
- JavaScript→Pythonのデータ転送チャネルが不安定
|
| 30 |
+
|
| 31 |
+
## 📚 refs/dwpose_modifierでの実装(重要!)
|
| 32 |
+
|
| 33 |
+
### 1. データ同期の仕組み
|
| 34 |
+
```python
|
| 35 |
+
# handlers/pose_edit_handlers.py (1000行目付近)
|
| 36 |
+
components['pose_json'].change(
|
| 37 |
+
fn=update_pose_data_from_canvas,
|
| 38 |
+
inputs=[components['pose_json'], components['frame_slider']],
|
| 39 |
+
outputs=[components['pose_json'], components.get('edited_jsonl_state', gr.State(""))] + all_components
|
| 40 |
+
)
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
**ポイント**:
|
| 44 |
+
- `pose_json`コンポーネントが中心
|
| 45 |
+
- changeイベントで`update_pose_data_from_canvas`関数を実行
|
| 46 |
+
- グローバル変数`_current_poses`で全データを保持
|
| 47 |
+
|
| 48 |
+
### 2. Canvas→Python データ転送
|
| 49 |
+
```python
|
| 50 |
+
def update_pose_data_from_canvas(pose_json_str, current_frame):
|
| 51 |
+
"""Canvasからのポーズデータ更新を受け取り、編集済みデータを更新"""
|
| 52 |
+
global _current_poses
|
| 53 |
+
|
| 54 |
+
# JSONデータをパース
|
| 55 |
+
canvas_data = json.loads(pose_json_str)
|
| 56 |
+
|
| 57 |
+
# Canvasデータから人物データを抽出
|
| 58 |
+
if 'people' in canvas_data and canvas_data['people']:
|
| 59 |
+
pose_data = canvas_data['people'][0]
|
| 60 |
+
|
| 61 |
+
# 現在のフレームのポーズデータを更新
|
| 62 |
+
if _current_poses and 0 <= current_frame < len(_current_poses):
|
| 63 |
+
_current_poses[current_frame]['people'] = [pose_data]
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
### 3. 強制changeイベント発火
|
| 67 |
+
```python
|
| 68 |
+
# タイムスタンプを追加してChangeイベントを確実に発火
|
| 69 |
+
import time
|
| 70 |
+
canvas_data['_t'] = int(time.time() * 1000)
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
### 4. JavaScript側の実装パターン
|
| 74 |
+
```javascript
|
| 75 |
+
// Canvas編集完了時にPythonへデータ送信
|
| 76 |
+
function sendToPython(poseData) {
|
| 77 |
+
// peopleフォーマットでデータを準備
|
| 78 |
+
const canvasData = {
|
| 79 |
+
"people": [{
|
| 80 |
+
"pose_keypoints_2d": [...],
|
| 81 |
+
"face_keypoints_2d": [...],
|
| 82 |
+
"hand_left_keypoints_2d": [...],
|
| 83 |
+
"hand_right_keypoints_2d": [...]
|
| 84 |
+
}],
|
| 85 |
+
"_t": Date.now() // タイムスタンプで強制イベント発火
|
| 86 |
+
};
|
| 87 |
+
|
| 88 |
+
// pose_jsonテキストボックスを更新
|
| 89 |
+
const poseJsonElement = document.getElementById('pose_json_data');
|
| 90 |
+
const textarea = poseJsonElement.querySelector('textarea');
|
| 91 |
+
textarea.value = JSON.stringify(canvasData);
|
| 92 |
+
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
| 93 |
+
}
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
## 🎯 修正すべき箇所
|
| 97 |
+
|
| 98 |
+
### 1. app.py
|
| 99 |
+
- [ ] グローバル変数での編集データ保持(refs同様)
|
| 100 |
+
- [ ] `update_pose_data_from_canvas`相当の関数実装
|
| 101 |
+
- [ ] pose_dataの更新ロジック改善
|
| 102 |
+
|
| 103 |
+
### 2. static/pose_editor.js
|
| 104 |
+
- [ ] `currentPoseData`未定義エラーの修正
|
| 105 |
+
- [ ] people形式での一貫したデータ管理
|
| 106 |
+
- [ ] 編集完了時の確実なデータ送信
|
| 107 |
+
|
| 108 |
+
### 3. データフロー
|
| 109 |
+
- [ ] bodies.candidate編集 → people形式変換 → Python送信
|
| 110 |
+
- [ ] タイムスタンプによる強制changeイベント
|
| 111 |
+
- [ ] グローバル編集データの永続化
|
| 112 |
+
|
| 113 |
+
## 💡 解決策の要点
|
| 114 |
+
|
| 115 |
+
1. **グローバル変数管理**
|
| 116 |
+
- Python側で編集中のポーズデータをグローバルに保持
|
| 117 |
+
- Canvas編集のたびに更新
|
| 118 |
+
|
| 119 |
+
2. **データ形式統一**
|
| 120 |
+
- 内部的にpeople形式で統一管理
|
| 121 |
+
- bodies.candidateは描画用のみ
|
| 122 |
+
|
| 123 |
+
3. **確実なイベント処理**
|
| 124 |
+
- タイムスタンプ追加で毎回changeイベント発火
|
| 125 |
+
- 専用のデータ転送チャネル確立
|
| 126 |
+
|
| 127 |
+
## 📝 これまでの試行錯誤
|
| 128 |
+
|
| 129 |
+
### 試したこと
|
| 130 |
+
1. `js_pose_update`隠しテキストボックス追加
|
| 131 |
+
2. `sendPoseDataToGradio`関数にタイムスタンプ追加
|
| 132 |
+
3. `syncBodiesToPeople`等の同期関数追加
|
| 133 |
+
|
| 134 |
+
### なぜ失敗したか
|
| 135 |
+
- データ同期のタイミングが不適切
|
| 136 |
+
- グローバル変数での管理不足
|
| 137 |
+
- refsの実装パターンを正確に再現できていない
|
| 138 |
+
|
| 139 |
+
## ⏰ 優先度
|
| 140 |
+
|
| 141 |
+
**Critical** - ���プリケーションの中核機能が動作しない致命的問題
|
| 142 |
+
|
| 143 |
+
## ✅ 完了条件
|
| 144 |
+
|
| 145 |
+
- [ ] Canvas編集がJSONダウンロードに反映される
|
| 146 |
+
- [ ] Canvas編集が画像エクスポートに反映される
|
| 147 |
+
- [ ] エラーなく安定して動作する
|
| 148 |
+
- [ ] 簡易モード・詳細モードの両方で動作
|
| 149 |
+
|
| 150 |
+
## 🔑 次回作業者への重要メモ
|
| 151 |
+
|
| 152 |
+
### 絶対に確認すべきrefs実装
|
| 153 |
+
1. `refs/dwpose_modifier/handlers/pose_edit_handlers.py`
|
| 154 |
+
- `update_pose_data_from_canvas`関数
|
| 155 |
+
- グローバル変数`_current_poses`の使い方
|
| 156 |
+
|
| 157 |
+
2. `refs/dwpose_modifier/ui/tab2_pose_edit.py`
|
| 158 |
+
- pose_jsonコンポーネントの定義
|
| 159 |
+
- changeイベントの登録方法
|
| 160 |
+
|
| 161 |
+
3. JavaScript側の実装(refsにはJSファイルがないが、app.py内のJS文字列を確認)
|
| 162 |
+
- pose_jsonへのデータ書き込み方法
|
| 163 |
+
- タイムスタンプ追加パターン
|
| 164 |
+
|
| 165 |
+
### 現在の状態(修正後)
|
| 166 |
+
- **app.py**: ✅ `_current_pose_data`グローバル変数追加、`on_js_pose_update`でpeople形式対応
|
| 167 |
+
- **pose_editor.js**: ✅ `currentPoseData`未定義エラー修正、`sendPoseDataToGradio`でpeople形式送信対応
|
| 168 |
+
- **データ同期**: 🔄 大幅に改善、people形式での双方向同期実装済み
|
| 169 |
+
|
| 170 |
+
### 主な修正内容(2025-01-13)
|
| 171 |
+
1. **app.py**: グローバル変数`_current_pose_data`追加、全読み込み・エクスポート関数でグローバル管理対応
|
| 172 |
+
2. **pose_editor.js**: `handleMouseUp`の`currentPoseData`未定義エラー修正
|
| 173 |
+
3. **データ送信**: `sendPoseDataToGradio`でpeople形式データ送信に対応(bodies.candidate→people変換)
|
| 174 |
+
4. **データ受信**: `on_js_pose_update`でpeople→bodies.candidate逆変換対応
|
| 175 |
+
|
| 176 |
+
## 🚨 新たに発生した問題(修正後)
|
| 177 |
+
|
| 178 |
+
### 1. キーポイント移動後に描画が消失 💥
|
| 179 |
+
- **現象**: キーポイントをドラッグして移動した後、Canvas上の描画が完全に消える
|
| 180 |
+
- **推定原因**: データ同期処理でposeDataが破壊されているか、描画関数に正しくデータが渡されていない
|
| 181 |
+
|
| 182 |
+
### 2. 手操作後の`currentPoseData`初期化エラー 🖐️💥
|
| 183 |
+
```javascript
|
| 184 |
+
Uncaught ReferenceError: Cannot access 'currentPoseData' before initialization
|
| 185 |
+
at HTMLCanvasElement.handleMouseDown (<anonymous>:444:39)
|
| 186 |
+
```
|
| 187 |
+
- **現象**: 手の矩形を操作した後、次のクリックで`handleMouseDown`でエラー発生
|
| 188 |
+
- **原因**: `handleMouseDown`関数内で`currentPoseData`が初期化前にアクセスされている
|
| 189 |
+
- **影響**: 手編集後にCanvas操作が不可能になる
|
| 190 |
+
|
| 191 |
+
### 3. ダウンロード画像が真っ黒 🖤
|
| 192 |
+
- **現象**: 画像ダウンロードボタンを押すと真っ黒な画像がダウンロードされる
|
| 193 |
+
- **推定原因**: グローバル変数のデータ形式が画像生成関数と互換性がない、またはデータが正しく渡されていない
|
| 194 |
+
- **影響**: 編集結果を画像として保存できない(機能完全停止)
|
| 195 |
+
|
| 196 |
+
## 🔧 緊急修正が必要な箇所
|
| 197 |
+
|
| 198 |
+
### 最優先修正項目
|
| 199 |
+
1. **handleMouseDown**の`currentPoseData`スコープエラー(444行目)
|
| 200 |
+
2. **描画データの保持**:編集後もCanvas描画を維持する機能
|
| 201 |
+
3. **画像エクスポート**:グローバルデータと画像生成の互換性確保
|
| 202 |
+
|
| 203 |
+
### データフロー問題の可能性
|
| 204 |
+
- people形式とbodies.candidate形式の変換で整合性が失われている
|
| 205 |
+
- グローバル変数の更新タイミングが描画タイミングと合っていない
|
| 206 |
+
- エクスポート関数が期待するデータ形式と実際のグローバルデータ形式が異なる
|
| 207 |
+
|
| 208 |
+
## 🔧 追加修正内容(refs issue043準拠)
|
| 209 |
+
|
| 210 |
+
### 二重ロック機構の実装(2025-01-13)
|
| 211 |
+
refs/dwpose_modifier/issues/043の解決策を適用し、JavaScript ⇄ Python間の同期ズレ問題を根本解決:
|
| 212 |
+
|
| 213 |
+
#### Python側ロック機構 (app.py)
|
| 214 |
+
- ✅ `_is_updating`グローバルフラグを追加
|
| 215 |
+
- ✅ `on_js_pose_update`関数にtry-finally制御を実装
|
| 216 |
+
- ✅ 処理中の新しい要求は即座にスキップ
|
| 217 |
+
- ✅ 例外発生時もfinallyブロックで確実にフラグ解除
|
| 218 |
+
|
| 219 |
+
#### JavaScript側ロック機構 (pose_editor.js)
|
| 220 |
+
- ✅ `window.poseEditorGlobals.isUpdating`フラグ使用
|
| 221 |
+
- ✅ `sendPoseDataToGradio`関数に同時実行防止制御を実装
|
| 222 |
+
- ✅ `gradioCanvasUpdate`にも既存のロック機構を確認済み
|
| 223 |
+
- ✅ try-finally制御で確実なフラグ管理
|
| 224 |
+
|
| 225 |
+
#### 処理フロー最適化
|
| 226 |
+
- ✅ 二重ロック(Python+JavaScript)で確実な同期制御
|
| 227 |
+
- ✅ 処理中は新しい要求をスキップして現在状態を維持
|
| 228 |
+
- ✅ UIの応答性を保ちながら競合状態を防止
|
| 229 |
+
|
| 230 |
+
### 期待される効果
|
| 231 |
+
- ✅ キーポイント移動後の描画消失防止
|
| 232 |
+
- ✅ 手操作後の`currentPoseData`エラー解消
|
| 233 |
+
- ✅ データ同期競合状態の根本解決
|
| 234 |
+
- ⏳ ダウンロード画像の黒画像問題(引き続き調査中)
|
| 235 |
+
|
| 236 |
+
---
|
| 237 |
+
|
| 238 |
+
## 🔍 詳細調査結果(2025-01-13)- 矩形編集の4大問題
|
| 239 |
+
|
| 240 |
+
### 📋 現在発生している問題
|
| 241 |
+
|
| 242 |
+
1. **顔の矩形編集**: 一度目は拡大縮小だけ正常、全体移動は矩形とキーポイントがズレる 📐💥
|
| 243 |
+
2. **手のキーポイント**: 全く動かない 🖐️❌
|
| 244 |
+
3. **矩形編集終了**: 矩形が編集前に戻る 🔄💥
|
| 245 |
+
4. **二度目の編集**: 全く動かなくなる 🚫💀
|
| 246 |
+
|
| 247 |
+
### 🔍 **問題1: 手のキーポイントが動かない原因**
|
| 248 |
+
|
| 249 |
+
#### **根本原因発見**: JavaScript側の手データアクセスが完全に破綻 💥
|
| 250 |
+
|
| 251 |
+
**問題箇所**: `pose_editor.js`の手データ参照方式
|
| 252 |
+
```javascript
|
| 253 |
+
// ❌ 現在の実装(動かない)
|
| 254 |
+
const originalHandData = currentPoseData.hands && currentPoseData.hands[detailKeypoint.handIndex];
|
| 255 |
+
|
| 256 |
+
// ✅ 正しい実装(refsと同じ)
|
| 257 |
+
const leftHandData = currentPoseData.people[0].hand_left_keypoints_2d;
|
| 258 |
+
const rightHandData = currentPoseData.people[0].hand_right_keypoints_2d;
|
| 259 |
+
```
|
| 260 |
+
|
| 261 |
+
**問題詳細**:
|
| 262 |
+
- テンプレートの手データは正常に読み込まれている ✅
|
| 263 |
+
- app.pyでのpeople形式変換も正常 ✅
|
| 264 |
+
- **JavaScript側で`currentPoseData.hands`に依存してるが、people形式では`hand_left_keypoints_2d`にある** 💥
|
| 265 |
+
|
| 266 |
+
**修正方針**: 手データアクセスを完全にpeople形式に統一
|
| 267 |
+
|
| 268 |
+
### 🔍 **問題2: 矩形移動でズレる原因**
|
| 269 |
+
|
| 270 |
+
#### **根本原因**: 累積移動計算とキーポイント変換の不整合
|
| 271 |
+
|
| 272 |
+
**問題箇所**: `updateRectMoveDrag`での座標計算
|
| 273 |
+
```javascript
|
| 274 |
+
// ❌ 現在の実装
|
| 275 |
+
const deltaX = mouseX - window.poseEditorGlobals.dragStartPos.x;
|
| 276 |
+
window.poseEditorGlobals.dragStartPos.x = mouseX; // 毎回更新で累積エラー
|
| 277 |
+
```
|
| 278 |
+
|
| 279 |
+
**修正済み**: 元座標からの総移動量計算に変更済み ✅
|
| 280 |
+
|
| 281 |
+
### 🔍 **問題3: 矩形編集終了で元に戻る原因**
|
| 282 |
+
|
| 283 |
+
#### **根本原因**: `drawPose`関数での矩形再計算
|
| 284 |
+
|
| 285 |
+
**問題の流れ**:
|
| 286 |
+
1. 矩形編集終了 → `rectEditModeActive = false`
|
| 287 |
+
2. `drawPose()` 呼び出し
|
| 288 |
+
3. `drawHandRectangles/drawFaceRectangles`で矩形再計算
|
| 289 |
+
4. **編集後のキーポイントから計算した矩形が、編集前の矩形サイズに戻る** 💥
|
| 290 |
+
|
| 291 |
+
**修正済み**: 編集モード中の矩形再計算スキップ機能追加 ✅
|
| 292 |
+
|
| 293 |
+
### 🔍 **問題4: 二度目の編集が動かない原因**
|
| 294 |
+
|
| 295 |
+
#### **根本原因**: 状態クリアとoriginalKeypointsの管理不備
|
| 296 |
+
|
| 297 |
+
**問題の流れ**:
|
| 298 |
+
1. 一度目編集後、`originalKeypoints`に編集済みデータが保存される
|
| 299 |
+
2. 二度目編集時、編集済みデータを「元データ」として使用
|
| 300 |
+
3. **元データと現在データが同じになり、変換しても変化なし** 💥
|
| 301 |
+
|
| 302 |
+
**修正済み**: `baseOriginalKeypoints`と状態完全クリア機能追加 ✅
|
| 303 |
+
|
| 304 |
+
### 🚀 **refs/dwpose_modifierとの実装差異分析**
|
| 305 |
+
|
| 306 |
+
#### **全体アーキテクチャの違い**
|
| 307 |
+
- **refs**: `window.poseEditorGlobals`で統一管理 ✨
|
| 308 |
+
- **現在**: 分散状態管理で複雑化 ❌
|
| 309 |
+
|
| 310 |
+
#### **キーポイント変換方式の違い**
|
| 311 |
+
- **refs**: 統一された`transformCoordinate`システム ✨
|
| 312 |
+
- **現在**: 座標変換ロジックが分散 ❌
|
| 313 |
+
|
| 314 |
+
#### **状態管理方式の違い**
|
| 315 |
+
- **refs**: シンプルな状態変数 ✨
|
| 316 |
+
- **現在**: 複数の状態変数が分散 ❌
|
| 317 |
+
|
| 318 |
+
#### **イベント処理フローの違い**
|
| 319 |
+
- **refs**: 段階的な優先順序で明確化 ✨
|
| 320 |
+
- **現在**: 複雑な条件分岐で重複 ❌
|
| 321 |
+
|
| 322 |
+
### 📋 **実装優先度リスト**
|
| 323 |
+
|
| 324 |
+
| 問題 | 優先度 | 修正状況 | 次のアクション |
|
| 325 |
+
|------|--------|----------|----------------|
|
| 326 |
+
| **手のキーポイント動かない** | 🔥 HIGH | ❌ 未修正 | people形式アクセスに統一 |
|
| 327 |
+
| **矩形移動ズレ** | 🔥 HIGH | ✅ 修正済み | テスト確認 |
|
| 328 |
+
| **矩形編集終了で戻る** | 🔥 HIGH | ✅ 修正済み | テスト確認 |
|
| 329 |
+
| **二度目編集不能** | 🔥 HIGH | ✅ 修正済み | テスト確認 |
|
| 330 |
+
|
| 331 |
+
### 🎯 **次回実装計画**
|
| 332 |
+
|
| 333 |
+
1. **手データアクセス修正** - `currentPoseData.hands` → `currentPoseData.people[0].hand_*_keypoints_2d`
|
| 334 |
+
2. **refs互換座標変換システム導入** - `transformCoordinate`関数移植
|
| 335 |
+
3. **グローバル状態管理統一** - `window.poseEditorGlobals`一本化
|
| 336 |
+
4. **イベント処理フロー簡素化** - refs方式の段階的処理採用
|
| 337 |
+
|
| 338 |
+
---
|
| 339 |
+
|
| 340 |
+
**作成日**: 2025-01-13
|
| 341 |
+
**担当**: Claude Code
|
| 342 |
+
**ステータス**: 🔄 調査完了・実装計画策定済み
|
| 343 |
+
**最終更新**: 2025-01-13 (詳細調査結果追加)
|
static/pose_editor.js
CHANGED
|
@@ -10,6 +10,8 @@ window.poseEditorGlobals = {
|
|
| 10 |
enableHands: true,
|
| 11 |
enableFace: true,
|
| 12 |
editMode: "簡易モード", // "簡易モード" or "詳細モード"
|
|
|
|
|
|
|
| 13 |
// 🔧 矩形編集状態(refs互換)
|
| 14 |
rectEditMode: null, // 'leftHand', 'rightHand', 'face', null
|
| 15 |
rectEditModeActive: false, // 矩形編集モード状態
|
|
@@ -250,6 +252,109 @@ function findNearestKeypoint(mouseX, mouseY, maxDistance = 20) {
|
|
| 250 |
return nearestIndex; // 🔧 refs互換:インデックス数値を返す
|
| 251 |
}
|
| 252 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
// マウスダウン処理(refs互換 + 矩形編集対応)
|
| 254 |
function handleMouseDown(event) {
|
| 255 |
if (!isCanvasReady()) {
|
|
@@ -266,28 +371,12 @@ function handleMouseDown(event) {
|
|
| 266 |
const controlPoint = findNearestRectControlPoint(mousePos.x, mousePos.y);
|
| 267 |
|
| 268 |
if (controlPoint) {
|
| 269 |
-
// 🔧
|
| 270 |
const rectType = window.poseEditorGlobals.rectEditMode;
|
| 271 |
const currentRect = window.poseEditorGlobals.currentRects[rectType];
|
| 272 |
if (currentRect) {
|
| 273 |
window.poseEditorGlobals.originalRect = { ...currentRect };
|
| 274 |
-
|
| 275 |
-
// 🔧 キーポイントの元座標も保存(refs互換)
|
| 276 |
-
const currentPoseData = window.poseEditorGlobals.poseData || poseData;
|
| 277 |
-
if (currentPoseData) {
|
| 278 |
-
window.poseEditorGlobals.originalKeypoints = JSON.parse(JSON.stringify(currentPoseData));
|
| 279 |
-
}
|
| 280 |
-
|
| 281 |
-
// 🔧 rectEditInfo初期化(キーポイントインデックス情報)
|
| 282 |
-
if (!window.poseEditorGlobals.rectEditInfo) {
|
| 283 |
-
window.poseEditorGlobals.rectEditInfo = {};
|
| 284 |
-
}
|
| 285 |
-
|
| 286 |
-
// 矩形タイプごとのキーポイントインデックスを設定
|
| 287 |
-
window.poseEditorGlobals.rectEditInfo[rectType] = {
|
| 288 |
-
originalRect: { ...currentRect },
|
| 289 |
-
keypointIndices: getRectKeypointIndices(rectType)
|
| 290 |
-
};
|
| 291 |
}
|
| 292 |
|
| 293 |
// コントロールポイントドラッグ(リサイズ)
|
|
@@ -300,23 +389,10 @@ function handleMouseDown(event) {
|
|
| 300 |
// 矩形内ドラッグ(移動)
|
| 301 |
const rectType = findRectContaining(mousePos.x, mousePos.y);
|
| 302 |
if (rectType === window.poseEditorGlobals.rectEditMode) {
|
| 303 |
-
// 🔧 矩形移動時もrectEditInfo初期化
|
| 304 |
-
const currentRect = window.poseEditorGlobals.currentRects[rectType];
|
| 305 |
-
if (currentRect) {
|
| 306 |
-
if (!window.poseEditorGlobals.rectEditInfo) {
|
| 307 |
-
window.poseEditorGlobals.rectEditInfo = {};
|
| 308 |
-
}
|
| 309 |
-
|
| 310 |
-
window.poseEditorGlobals.rectEditInfo[rectType] = {
|
| 311 |
-
originalRect: { ...currentRect },
|
| 312 |
-
keypointIndices: getRectKeypointIndices(rectType)
|
| 313 |
-
};
|
| 314 |
-
|
| 315 |
-
}
|
| 316 |
-
|
| 317 |
window.poseEditorGlobals.draggedRect = rectType;
|
| 318 |
window.poseEditorGlobals.dragStartPos = { x: mousePos.x, y: mousePos.y };
|
| 319 |
isDragging = true;
|
|
|
|
| 320 |
return;
|
| 321 |
}
|
| 322 |
|
|
@@ -334,14 +410,30 @@ function handleMouseDown(event) {
|
|
| 334 |
|
| 335 |
// 矩形外クリック → 編集モード終了
|
| 336 |
|
| 337 |
-
// 🚀
|
|
|
|
|
|
|
|
|
|
| 338 |
sendPoseDataToGradio();
|
| 339 |
|
|
|
|
| 340 |
window.poseEditorGlobals.rectEditModeActive = false;
|
| 341 |
window.poseEditorGlobals.rectEditMode = null;
|
| 342 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 343 |
// 🚀 矩形位置を保持したまま再描画(再計算させない)
|
| 344 |
-
const currentPoseData = window.poseEditorGlobals.poseData || poseData;
|
| 345 |
if (currentPoseData) {
|
| 346 |
// 矩形編集モードをfalseにした直後なので、矩形は保持される
|
| 347 |
drawPose(currentPoseData, window.poseEditorGlobals.enableHands, window.poseEditorGlobals.enableFace);
|
|
@@ -349,12 +441,17 @@ function handleMouseDown(event) {
|
|
| 349 |
return;
|
| 350 |
}
|
| 351 |
|
| 352 |
-
// 矩形編集モードでない場合の矩形クリック →
|
| 353 |
const rectType = findRectContaining(mousePos.x, mousePos.y);
|
| 354 |
if (rectType) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 355 |
window.poseEditorGlobals.rectEditModeActive = true;
|
| 356 |
-
|
| 357 |
-
debugLog(`Entered rectangle edit mode: ${rectType}`);
|
| 358 |
|
| 359 |
// 🔧 rectEditInfo全体を初期化(全矩形タイプ対応)
|
| 360 |
initializeRectEditInfo();
|
|
@@ -368,44 +465,671 @@ function handleMouseDown(event) {
|
|
| 368 |
}
|
| 369 |
}
|
| 370 |
|
| 371 |
-
// 🔧 詳細モードまたは矩形編集モードでない場合:通常のキーポイント編集
|
| 372 |
-
if (window.poseEditorGlobals.editMode === "詳細モード" || !window.poseEditorGlobals.rectEditModeActive) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 373 |
|
| 374 |
-
|
|
|
|
|
|
|
|
|
|
| 375 |
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
const originalRes = currentPoseData.resolution || [512, 512];
|
| 385 |
-
const scaleX = canvas.width / originalRes[0];
|
| 386 |
-
const scaleY = canvas.height / originalRes[1];
|
| 387 |
-
|
| 388 |
-
const point = candidates[keypointIndex];
|
| 389 |
-
if (point) {
|
| 390 |
-
const keypointX = point[0] * scaleX;
|
| 391 |
-
const keypointY = point[1] * scaleY;
|
| 392 |
-
|
| 393 |
-
dragOffset = {
|
| 394 |
-
x: mousePos.x - keypointX,
|
| 395 |
-
y: mousePos.y - keypointY
|
| 396 |
-
};
|
| 397 |
-
|
| 398 |
-
}
|
| 399 |
-
}
|
| 400 |
-
|
| 401 |
-
debugLog(`Drag started: keypoint ${keypointIndex} at (${mousePos.x}, ${mousePos.y})`);
|
| 402 |
-
|
| 403 |
-
// カーソルを変更
|
| 404 |
-
canvas.style.cursor = 'grabbing';
|
| 405 |
-
} else {
|
| 406 |
-
debugLog("No keypoint found near mouse position");
|
| 407 |
}
|
| 408 |
}
|
|
|
|
|
|
|
| 409 |
}
|
| 410 |
|
| 411 |
// マウス移動処理(refs互換 + 矩形編集対応)
|
|
@@ -427,19 +1151,54 @@ function handleMouseMove(event) {
|
|
| 427 |
return;
|
| 428 |
}
|
| 429 |
|
| 430 |
-
//
|
| 431 |
-
if (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 432 |
// 🔧 refs互換:オフセット考慮の新座標計算
|
| 433 |
const newX = mousePos.x - dragOffset.x;
|
| 434 |
const newY = mousePos.y - dragOffset.y;
|
| 435 |
|
| 436 |
-
|
| 437 |
// ドラッグ中の座標更新(オフセット適用済み座標で)
|
| 438 |
updateKeypointPosition(draggedKeypoint, newX, newY);
|
| 439 |
|
| 440 |
-
// リアルタイム再描画(ハイライト付き)-
|
| 441 |
-
|
| 442 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 443 |
}
|
| 444 |
|
| 445 |
} else {
|
|
@@ -454,9 +1213,14 @@ function handleMouseMove(event) {
|
|
| 454 |
canvas.style.cursor = rectType === window.poseEditorGlobals.rectEditMode ? 'move' : 'default';
|
| 455 |
}
|
| 456 |
} else {
|
| 457 |
-
//
|
| 458 |
-
|
| 459 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 460 |
}
|
| 461 |
}
|
| 462 |
}
|
|
@@ -480,6 +1244,10 @@ function handleMouseUp(event) {
|
|
| 480 |
} else if (window.poseEditorGlobals.draggedRect) {
|
| 481 |
debugLog(`Rectangle move drag ended: ${window.poseEditorGlobals.draggedRect}`);
|
| 482 |
window.poseEditorGlobals.draggedRect = null;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 483 |
} else if (draggedKeypoint >= 0) {
|
| 484 |
draggedKeypoint = -1;
|
| 485 |
}
|
|
@@ -487,12 +1255,12 @@ function handleMouseUp(event) {
|
|
| 487 |
isDragging = false;
|
| 488 |
canvas.style.cursor = 'default';
|
| 489 |
|
| 490 |
-
// Gradio
|
| 491 |
sendPoseDataToGradio();
|
| 492 |
}
|
| 493 |
}
|
| 494 |
|
| 495 |
-
// キーポイント座標更新(refs
|
| 496 |
function updateKeypointPosition(keypointIndex, canvasX, canvasY) {
|
| 497 |
// グローバルposeDataを参照
|
| 498 |
const currentPoseData = window.poseEditorGlobals.poseData || poseData;
|
|
@@ -515,44 +1283,263 @@ function updateKeypointPosition(keypointIndex, canvasX, canvasY) {
|
|
| 515 |
const clampedDataX = Math.max(0, Math.min(originalRes[0], dataX));
|
| 516 |
const clampedDataY = Math.max(0, Math.min(originalRes[1], dataY));
|
| 517 |
|
| 518 |
-
// candidateリストを更新
|
| 519 |
const candidates = currentPoseData.bodies.candidate;
|
| 520 |
if (keypointIndex < candidates.length) {
|
| 521 |
candidates[keypointIndex][0] = clampedDataX;
|
| 522 |
candidates[keypointIndex][1] = clampedDataY;
|
| 523 |
|
| 524 |
-
|
| 525 |
// 🔧 ローカルposeDataも同期更新
|
| 526 |
if (poseData && poseData.bodies && poseData.bodies.candidate) {
|
| 527 |
poseData.bodies.candidate[keypointIndex][0] = clampedDataX;
|
| 528 |
poseData.bodies.candidate[keypointIndex][1] = clampedDataY;
|
| 529 |
}
|
| 530 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 531 |
}
|
| 532 |
|
| 533 |
-
// Gradioにデータ送信(refs
|
| 534 |
function sendPoseDataToGradio() {
|
|
|
|
| 535 |
// グローバルposeDataを参照
|
| 536 |
const currentPoseData = window.poseEditorGlobals.poseData || poseData;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 537 |
|
| 538 |
if (!currentPoseData) {
|
| 539 |
debugLog("No poseData available for Gradio send");
|
| 540 |
return;
|
| 541 |
}
|
| 542 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 543 |
try {
|
| 544 |
-
//
|
| 545 |
-
const
|
|
|
|
|
|
|
|
|
|
| 546 |
|
| 547 |
-
//
|
| 548 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 549 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 550 |
for (const textarea of textareas) {
|
| 551 |
const currentValue = textarea.value || '';
|
| 552 |
if (currentValue.includes('bodies') || currentValue.includes('candidate') || currentValue.trim() === '') {
|
| 553 |
textarea.value = jsonString;
|
| 554 |
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
| 555 |
-
debugLog('Pose data sent to Gradio
|
| 556 |
return;
|
| 557 |
}
|
| 558 |
}
|
|
@@ -560,6 +1547,13 @@ function sendPoseDataToGradio() {
|
|
| 560 |
|
| 561 |
} catch (error) {
|
| 562 |
debugLog(`Error sending data to Gradio: ${error.message}`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 563 |
}
|
| 564 |
}
|
| 565 |
|
|
@@ -582,10 +1576,17 @@ function drawPose(poseData, enableHands = true, enableFace = true, highlightInde
|
|
| 582 |
// キャンバスクリア
|
| 583 |
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
| 584 |
|
|
|
|
|
|
|
|
|
|
| 585 |
// 🔧 矩形編集モード中でない場合のみ矩形を再計算
|
| 586 |
if (window.poseEditorGlobals.editMode === "簡易モード" && !window.poseEditorGlobals.rectEditModeActive) {
|
| 587 |
window.poseEditorGlobals.currentRects = { leftHand: null, rightHand: null, face: null };
|
| 588 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 589 |
|
| 590 |
// 📐 解像度情報の取得(手と顔描画のため)
|
| 591 |
const originalRes = currentPoseData.resolution || [512, 512];
|
|
@@ -596,16 +1597,30 @@ function drawPose(poseData, enableHands = true, enableFace = true, highlightInde
|
|
| 596 |
// ボディの描画(ハイライト対応)
|
| 597 |
drawBody(currentPoseData, highlightIndex);
|
| 598 |
|
| 599 |
-
//
|
| 600 |
-
if (enableHands
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 609 |
|
| 610 |
// 🔧 簡易モード:手の矩形描画(編集モード中は再計算しない)
|
| 611 |
if (window.poseEditorGlobals.editMode === "簡易モード") {
|
|
@@ -614,12 +1629,13 @@ function drawPose(poseData, enableHands = true, enableFace = true, highlightInde
|
|
| 614 |
if (currentPoseData.people && currentPoseData.people[0]) {
|
| 615 |
const person = currentPoseData.people[0];
|
| 616 |
const editedHandsData = [
|
| 617 |
-
person.hand_left_keypoints_2d ||
|
| 618 |
-
person.hand_right_keypoints_2d ||
|
| 619 |
];
|
| 620 |
drawHandRectangles(editedHandsData, originalRes, scaleX, scaleY);
|
| 621 |
} else {
|
| 622 |
-
|
|
|
|
| 623 |
}
|
| 624 |
} else {
|
| 625 |
// 編集モード中:既存の矩形を描画(再計算��ない)
|
|
@@ -632,13 +1648,27 @@ function drawPose(poseData, enableHands = true, enableFace = true, highlightInde
|
|
| 632 |
debugLog("No hand data available");
|
| 633 |
}
|
| 634 |
|
| 635 |
-
//
|
| 636 |
-
if (enableFace
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 642 |
|
| 643 |
// 🔧 簡易モード:顔の矩形描画(編集モード中は再計算しない)
|
| 644 |
if (window.poseEditorGlobals.editMode === "簡易モード") {
|
|
@@ -662,6 +1692,50 @@ function drawPose(poseData, enableHands = true, enableFace = true, highlightInde
|
|
| 662 |
}
|
| 663 |
}
|
| 664 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 665 |
// ボディ描画(ハイライト対応)
|
| 666 |
function drawBody(poseData, highlightIndex = -1) {
|
| 667 |
if (!poseData.bodies || !poseData.bodies.candidate) {
|
|
@@ -1010,9 +2084,87 @@ window.gradioCanvasUpdate = function(pose_json_str) {
|
|
| 1010 |
poseData = pose_json_str;
|
| 1011 |
}
|
| 1012 |
|
| 1013 |
-
//
|
| 1014 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1015 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1016 |
|
| 1017 |
if (!isCanvasReady()) {
|
| 1018 |
debugLog("Canvas not ready, initializing...");
|
|
@@ -1077,10 +2229,40 @@ window.updateDisplaySettings = function(enableHands, enableFace, editMode) {
|
|
| 1077 |
window.poseEditorGlobals.enableFace = enableFace;
|
| 1078 |
window.poseEditorGlobals.editMode = editMode;
|
| 1079 |
|
| 1080 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1081 |
const currentPoseData = window.poseEditorGlobals.poseData || poseData;
|
| 1082 |
if (currentPoseData && Object.keys(currentPoseData).length > 0) {
|
|
|
|
| 1083 |
drawPose(currentPoseData, enableHands, enableFace);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1084 |
}
|
| 1085 |
|
| 1086 |
};
|
|
@@ -1097,6 +2279,47 @@ window.showToast = function(type, message) {
|
|
| 1097 |
}
|
| 1098 |
};
|
| 1099 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1100 |
// Canvas操作時の通知
|
| 1101 |
function notifyCanvasOperation(message) {
|
| 1102 |
showToast('info', message);
|
|
@@ -1159,6 +2382,8 @@ function safeCanvasOperation(operation) {
|
|
| 1159 |
function drawHandRectangles(handsData, originalRes, scaleX, scaleY) {
|
| 1160 |
if (!handsData || handsData.length === 0) return;
|
| 1161 |
|
|
|
|
|
|
|
| 1162 |
const ctx = window.poseEditorGlobals.ctx;
|
| 1163 |
|
| 1164 |
// 矩形の色定義(refs互換)
|
|
@@ -1171,10 +2396,17 @@ function drawHandRectangles(handsData, originalRes, scaleX, scaleY) {
|
|
| 1171 |
if (rect) {
|
| 1172 |
const handType = HAND_TYPES[handIndex] || `hand_${handIndex}`;
|
| 1173 |
|
| 1174 |
-
// 🔧
|
| 1175 |
-
window.poseEditorGlobals.currentRects[handType]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1176 |
|
| 1177 |
-
|
|
|
|
|
|
|
| 1178 |
}
|
| 1179 |
}
|
| 1180 |
});
|
|
@@ -1184,6 +2416,8 @@ function drawHandRectangles(handsData, originalRes, scaleX, scaleY) {
|
|
| 1184 |
function drawFaceRectangles(facesData, originalRes, scaleX, scaleY) {
|
| 1185 |
if (!facesData || facesData.length === 0) return;
|
| 1186 |
|
|
|
|
|
|
|
| 1187 |
const ctx = window.poseEditorGlobals.ctx;
|
| 1188 |
|
| 1189 |
// 矩形の色定義(refs互換)
|
|
@@ -1193,10 +2427,17 @@ function drawFaceRectangles(facesData, originalRes, scaleX, scaleY) {
|
|
| 1193 |
if (face && face.length > 0) {
|
| 1194 |
const rect = calculateFaceRect(face, originalRes, scaleX, scaleY);
|
| 1195 |
if (rect) {
|
| 1196 |
-
// 🔧
|
| 1197 |
-
window.poseEditorGlobals.currentRects.face
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1198 |
|
| 1199 |
-
|
|
|
|
|
|
|
| 1200 |
}
|
| 1201 |
}
|
| 1202 |
}
|
|
@@ -1347,8 +2588,14 @@ function redrawPoseWithoutRecalculation() {
|
|
| 1347 |
drawBody(currentPoseData, -1);
|
| 1348 |
|
| 1349 |
// 手の描画(設定制御・座標変換パラメータ付き)
|
| 1350 |
-
|
| 1351 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1352 |
}
|
| 1353 |
|
| 1354 |
// 顔の描画(設定制御・座標変換パラメータ付き)
|
|
@@ -1545,8 +2792,10 @@ function updateRectControlDrag(mouseX, mouseY) {
|
|
| 1545 |
window.poseEditorGlobals.currentRects[rectType] = newRect;
|
| 1546 |
|
| 1547 |
|
| 1548 |
-
// 🔧
|
| 1549 |
-
|
|
|
|
|
|
|
| 1550 |
|
| 1551 |
// 🚀 refs互換:編集中もリアルタイム描画
|
| 1552 |
const currentPoseData = window.poseEditorGlobals.poseData || poseData;
|
|
@@ -1563,9 +2812,11 @@ function updateRectControlDrag(mouseX, mouseY) {
|
|
| 1563 |
function initializeRectEditInfo() {
|
| 1564 |
debugLog('🚀 Initializing rectEditInfo for all rect types');
|
| 1565 |
|
| 1566 |
-
|
| 1567 |
-
|
| 1568 |
-
|
|
|
|
|
|
|
| 1569 |
|
| 1570 |
const rectTypes = ['face', 'leftHand', 'rightHand'];
|
| 1571 |
|
|
@@ -1580,11 +2831,40 @@ function initializeRectEditInfo() {
|
|
| 1580 |
}
|
| 1581 |
}
|
| 1582 |
|
| 1583 |
-
//
|
| 1584 |
const currentPoseData = window.poseEditorGlobals.poseData || poseData;
|
| 1585 |
if (currentPoseData) {
|
| 1586 |
-
|
| 1587 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1588 |
}
|
| 1589 |
}
|
| 1590 |
|
|
@@ -1753,7 +3033,7 @@ function moveKeypointsArray(keypointsArray, deltaX, deltaY, label) {
|
|
| 1753 |
|
| 1754 |
}
|
| 1755 |
|
| 1756 |
-
// 🔧 矩形移動ドラッグ処理(refs
|
| 1757 |
function updateRectMoveDrag(mouseX, mouseY) {
|
| 1758 |
const rectType = window.poseEditorGlobals.draggedRect;
|
| 1759 |
if (!rectType) return;
|
|
@@ -1761,29 +3041,29 @@ function updateRectMoveDrag(mouseX, mouseY) {
|
|
| 1761 |
const rect = window.poseEditorGlobals.currentRects[rectType];
|
| 1762 |
if (!rect) return;
|
| 1763 |
|
| 1764 |
-
|
| 1765 |
-
const deltaX = mouseX -
|
| 1766 |
-
const deltaY = mouseY -
|
| 1767 |
|
| 1768 |
-
|
| 1769 |
-
window.poseEditorGlobals.dragStartPos.x = mouseX;
|
| 1770 |
-
window.poseEditorGlobals.dragStartPos.y = mouseY;
|
| 1771 |
|
| 1772 |
-
//
|
| 1773 |
-
|
| 1774 |
-
|
|
|
|
|
|
|
|
|
|
| 1775 |
|
| 1776 |
// Canvas境界制限
|
| 1777 |
const canvas = window.poseEditorGlobals.canvas;
|
| 1778 |
-
|
| 1779 |
-
|
| 1780 |
|
| 1781 |
// 矩形を更新
|
| 1782 |
-
window.poseEditorGlobals.currentRects[rectType] =
|
| 1783 |
-
|
| 1784 |
|
| 1785 |
-
//
|
| 1786 |
-
|
| 1787 |
|
| 1788 |
// 🚀 refs互換:編集中もリアルタイム描画
|
| 1789 |
const currentPoseData = window.poseEditorGlobals.poseData || poseData;
|
|
@@ -1794,6 +3074,9 @@ function updateRectMoveDrag(mouseX, mouseY) {
|
|
| 1794 |
window.poseEditorGlobals.enableFace
|
| 1795 |
);
|
| 1796 |
}
|
|
|
|
|
|
|
|
|
|
| 1797 |
}
|
| 1798 |
|
| 1799 |
// 🔧 矩形タイプに応じた色取得
|
|
@@ -1848,14 +3131,15 @@ function updateKeypointsByRect(rectType, newRect) {
|
|
| 1848 |
}
|
| 1849 |
}
|
| 1850 |
|
| 1851 |
-
//
|
| 1852 |
-
if (!targetKeypoints) {
|
| 1853 |
-
|
| 1854 |
-
|
| 1855 |
-
|
| 1856 |
-
|
| 1857 |
-
|
| 1858 |
-
|
|
|
|
| 1859 |
}
|
| 1860 |
}
|
| 1861 |
|
|
@@ -1996,14 +3280,15 @@ function moveKeypointsByRect(rectType, deltaX, deltaY) {
|
|
| 1996 |
}
|
| 1997 |
}
|
| 1998 |
|
| 1999 |
-
//
|
| 2000 |
-
if (!targetKeypoints) {
|
| 2001 |
-
|
| 2002 |
-
|
| 2003 |
-
|
| 2004 |
-
|
| 2005 |
-
|
| 2006 |
-
|
|
|
|
| 2007 |
}
|
| 2008 |
}
|
| 2009 |
|
|
|
|
| 10 |
enableHands: true,
|
| 11 |
enableFace: true,
|
| 12 |
editMode: "簡易モード", // "簡易モード" or "詳細モード"
|
| 13 |
+
// 🎨 背景画像機能
|
| 14 |
+
backgroundImage: null, // 背景画像オブジェクト
|
| 15 |
// 🔧 矩形編集状態(refs互換)
|
| 16 |
rectEditMode: null, // 'leftHand', 'rightHand', 'face', null
|
| 17 |
rectEditModeActive: false, // 矩形編集モード状態
|
|
|
|
| 252 |
return nearestIndex; // 🔧 refs互換:インデックス数値を返す
|
| 253 |
}
|
| 254 |
|
| 255 |
+
// 🎯 詳細モード用:手と顔のキーポイント検索
|
| 256 |
+
function findNearestKeypointInDetailMode(mouseX, mouseY, maxDistance = 15) {
|
| 257 |
+
const currentPoseData = window.poseEditorGlobals.poseData || poseData;
|
| 258 |
+
|
| 259 |
+
if (!currentPoseData) {
|
| 260 |
+
return null;
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
const originalRes = currentPoseData.resolution || [512, 512];
|
| 264 |
+
const scaleX = canvas.width / originalRes[0];
|
| 265 |
+
const scaleY = canvas.height / originalRes[1];
|
| 266 |
+
|
| 267 |
+
let nearestKeypoint = null;
|
| 268 |
+
let minDistance = maxDistance;
|
| 269 |
+
|
| 270 |
+
// 💖 手のキーポイント検索(people形式統一)
|
| 271 |
+
if (window.poseEditorGlobals.enableHands && currentPoseData.people && currentPoseData.people[0]) {
|
| 272 |
+
const person = currentPoseData.people[0];
|
| 273 |
+
const handsData = [
|
| 274 |
+
person.hand_left_keypoints_2d || [],
|
| 275 |
+
person.hand_right_keypoints_2d || []
|
| 276 |
+
];
|
| 277 |
+
|
| 278 |
+
['left', 'right'].forEach((handType, handIndex) => {
|
| 279 |
+
const handData = handsData[handIndex];
|
| 280 |
+
if (handData && handData.length > 0) {
|
| 281 |
+
for (let i = 0; i < handData.length; i += 3) {
|
| 282 |
+
if (i + 2 < handData.length) {
|
| 283 |
+
const x = handData[i] * scaleX;
|
| 284 |
+
const y = handData[i + 1] * scaleY;
|
| 285 |
+
const conf = handData[i + 2];
|
| 286 |
+
|
| 287 |
+
if (conf > 0.3) {
|
| 288 |
+
const distance = Math.sqrt((mouseX - x) ** 2 + (mouseY - y) ** 2);
|
| 289 |
+
if (distance < minDistance) {
|
| 290 |
+
minDistance = distance;
|
| 291 |
+
nearestKeypoint = {
|
| 292 |
+
type: 'hand',
|
| 293 |
+
handType: handType,
|
| 294 |
+
handIndex: handIndex,
|
| 295 |
+
keypointIndex: i / 3,
|
| 296 |
+
arrayIndex: i
|
| 297 |
+
};
|
| 298 |
+
}
|
| 299 |
+
}
|
| 300 |
+
}
|
| 301 |
+
}
|
| 302 |
+
}
|
| 303 |
+
});
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
// 😊 顔のキーポイント検索
|
| 307 |
+
if (window.poseEditorGlobals.enableFace && currentPoseData.faces) {
|
| 308 |
+
const facesData = currentPoseData.people && currentPoseData.people[0] && currentPoseData.people[0].face_keypoints_2d
|
| 309 |
+
? [currentPoseData.people[0].face_keypoints_2d]
|
| 310 |
+
: currentPoseData.faces;
|
| 311 |
+
|
| 312 |
+
if (facesData && facesData[0] && facesData[0].length > 0) {
|
| 313 |
+
const faceData = facesData[0];
|
| 314 |
+
for (let i = 0; i < faceData.length; i += 3) {
|
| 315 |
+
if (i + 2 < faceData.length) {
|
| 316 |
+
const x = faceData[i] * scaleX;
|
| 317 |
+
const y = faceData[i + 1] * scaleY;
|
| 318 |
+
const conf = faceData[i + 2];
|
| 319 |
+
|
| 320 |
+
if (conf > 0.3) {
|
| 321 |
+
const distance = Math.sqrt((mouseX - x) ** 2 + (mouseY - y) ** 2);
|
| 322 |
+
if (distance < minDistance) {
|
| 323 |
+
minDistance = distance;
|
| 324 |
+
nearestKeypoint = {
|
| 325 |
+
type: 'face',
|
| 326 |
+
keypointIndex: i / 3,
|
| 327 |
+
arrayIndex: i
|
| 328 |
+
};
|
| 329 |
+
}
|
| 330 |
+
}
|
| 331 |
+
}
|
| 332 |
+
}
|
| 333 |
+
}
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
// 👤 ボディのキーポイントも検索(優先度は低く)
|
| 337 |
+
const bodyKeypointIndex = findNearestKeypoint(mouseX, mouseY, maxDistance * 0.8);
|
| 338 |
+
if (bodyKeypointIndex >= 0) {
|
| 339 |
+
const candidates = currentPoseData.bodies.candidate;
|
| 340 |
+
const point = candidates[bodyKeypointIndex];
|
| 341 |
+
if (point) {
|
| 342 |
+
const scaledX = point[0] * scaleX;
|
| 343 |
+
const scaledY = point[1] * scaleY;
|
| 344 |
+
const distance = Math.sqrt((mouseX - scaledX) ** 2 + (mouseY - scaledY) ** 2);
|
| 345 |
+
|
| 346 |
+
if (distance < minDistance) {
|
| 347 |
+
nearestKeypoint = {
|
| 348 |
+
type: 'body',
|
| 349 |
+
keypointIndex: bodyKeypointIndex
|
| 350 |
+
};
|
| 351 |
+
}
|
| 352 |
+
}
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
return nearestKeypoint;
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
// マウスダウン処理(refs互換 + 矩形編集対応)
|
| 359 |
function handleMouseDown(event) {
|
| 360 |
if (!isCanvasReady()) {
|
|
|
|
| 371 |
const controlPoint = findNearestRectControlPoint(mousePos.x, mousePos.y);
|
| 372 |
|
| 373 |
if (controlPoint) {
|
| 374 |
+
// 🔧 コントロールポイントドラッグ開始時の元座標保存
|
| 375 |
const rectType = window.poseEditorGlobals.rectEditMode;
|
| 376 |
const currentRect = window.poseEditorGlobals.currentRects[rectType];
|
| 377 |
if (currentRect) {
|
| 378 |
window.poseEditorGlobals.originalRect = { ...currentRect };
|
| 379 |
+
debugLog(`🔧 Original rect saved for control point drag: ${rectType}`, window.poseEditorGlobals.originalRect);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 380 |
}
|
| 381 |
|
| 382 |
// コントロールポイントドラッグ(リサイズ)
|
|
|
|
| 389 |
// 矩形内ドラッグ(移動)
|
| 390 |
const rectType = findRectContaining(mousePos.x, mousePos.y);
|
| 391 |
if (rectType === window.poseEditorGlobals.rectEditMode) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 392 |
window.poseEditorGlobals.draggedRect = rectType;
|
| 393 |
window.poseEditorGlobals.dragStartPos = { x: mousePos.x, y: mousePos.y };
|
| 394 |
isDragging = true;
|
| 395 |
+
debugLog(`🔧 Rectangle move drag started: ${rectType}`);
|
| 396 |
return;
|
| 397 |
}
|
| 398 |
|
|
|
|
| 410 |
|
| 411 |
// 矩形外クリック → 編集モード終了
|
| 412 |
|
| 413 |
+
// 🚀 currentPoseData定義を先に移動
|
| 414 |
+
const currentPoseData = window.poseEditorGlobals.poseData || poseData;
|
| 415 |
+
|
| 416 |
+
// 🚀 編集完了データをGradio送信(データ改変はしない)
|
| 417 |
sendPoseDataToGradio();
|
| 418 |
|
| 419 |
+
// 🔧 矩形編集モード終了時の完全な状態クリア(連続編集対応)
|
| 420 |
window.poseEditorGlobals.rectEditModeActive = false;
|
| 421 |
window.poseEditorGlobals.rectEditMode = null;
|
| 422 |
|
| 423 |
+
// 💖 編集用データ状態をクリア(baseOriginalKeypointsは保持!)
|
| 424 |
+
// window.poseEditorGlobals.baseOriginalKeypoints = null; // ← 💥 これが原因!削除
|
| 425 |
+
window.poseEditorGlobals.originalKeypoints = null;
|
| 426 |
+
window.poseEditorGlobals.originalRect = null;
|
| 427 |
+
window.poseEditorGlobals.rectEditInfo = null;
|
| 428 |
+
|
| 429 |
+
// 🔧 ドラッグ状態もクリア
|
| 430 |
+
window.poseEditorGlobals.draggedRectControl = null;
|
| 431 |
+
window.poseEditorGlobals.draggedRect = null;
|
| 432 |
+
window.poseEditorGlobals.dragStartPos = { x: 0, y: 0 };
|
| 433 |
+
|
| 434 |
+
debugLog("🔧 Rectangle edit mode ended - all editing states cleared for next editing session");
|
| 435 |
+
|
| 436 |
// 🚀 矩形位置を保持したまま再描画(再計算させない)
|
|
|
|
| 437 |
if (currentPoseData) {
|
| 438 |
// 矩形編集モードをfalseにした直後なので、矩形は保持される
|
| 439 |
drawPose(currentPoseData, window.poseEditorGlobals.enableHands, window.poseEditorGlobals.enableFace);
|
|
|
|
| 441 |
return;
|
| 442 |
}
|
| 443 |
|
| 444 |
+
// 💖 矩形編集モードでない場合の矩形クリック → 編集モード開始(refs準拠)
|
| 445 |
const rectType = findRectContaining(mousePos.x, mousePos.y);
|
| 446 |
if (rectType) {
|
| 447 |
+
// 矩形モード切り替え時の処理
|
| 448 |
+
if (window.poseEditorGlobals.rectEditMode !== rectType) {
|
| 449 |
+
debugLog(`💖 Switching rectangle edit mode: ${window.poseEditorGlobals.rectEditMode} → ${rectType}`);
|
| 450 |
+
window.poseEditorGlobals.rectEditMode = rectType;
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
window.poseEditorGlobals.rectEditModeActive = true;
|
| 454 |
+
debugLog(`💖 Entered rectangle edit mode: ${rectType}`);
|
|
|
|
| 455 |
|
| 456 |
// 🔧 rectEditInfo全体を初期化(全矩形タイプ対応)
|
| 457 |
initializeRectEditInfo();
|
|
|
|
| 465 |
}
|
| 466 |
}
|
| 467 |
|
| 468 |
+
// 🔧 詳細モードまたは矩形編集モードでない場合:通常のキーポイント編集
|
| 469 |
+
if (window.poseEditorGlobals.editMode === "詳細モード" || !window.poseEditorGlobals.rectEditModeActive) {
|
| 470 |
+
|
| 471 |
+
// 🎯 詳細モードでは手・顔・ボディ全て検索
|
| 472 |
+
if (window.poseEditorGlobals.editMode === "詳細モード") {
|
| 473 |
+
const detailKeypoint = findNearestKeypointInDetailMode(mousePos.x, mousePos.y);
|
| 474 |
+
|
| 475 |
+
if (detailKeypoint) {
|
| 476 |
+
isDragging = true;
|
| 477 |
+
window.poseEditorGlobals.draggedDetailKeypoint = detailKeypoint;
|
| 478 |
+
debugLog(`🎯 Detail mode drag started: ${detailKeypoint.type} keypoint ${detailKeypoint.keypointIndex}`);
|
| 479 |
+
canvas.style.cursor = 'grabbing';
|
| 480 |
+
return;
|
| 481 |
+
}
|
| 482 |
+
}
|
| 483 |
+
// 🔧 簡易モードまたは詳細モードで何も見つからない場合:ボディキーポイント検索
|
| 484 |
+
else {
|
| 485 |
+
const keypointIndex = findNearestKeypoint(mousePos.x, mousePos.y);
|
| 486 |
+
|
| 487 |
+
if (keypointIndex >= 0) {
|
| 488 |
+
isDragging = true;
|
| 489 |
+
draggedKeypoint = keypointIndex;
|
| 490 |
+
|
| 491 |
+
// 🔧 refs互換:ドラッグオフセット計算(キーポイントの正確な位置からのオフセット)
|
| 492 |
+
const currentPoseData = window.poseEditorGlobals.poseData || poseData;
|
| 493 |
+
if (currentPoseData && currentPoseData.bodies && currentPoseData.bodies.candidate) {
|
| 494 |
+
const candidates = currentPoseData.bodies.candidate;
|
| 495 |
+
const originalRes = currentPoseData.resolution || [512, 512];
|
| 496 |
+
const scaleX = canvas.width / originalRes[0];
|
| 497 |
+
const scaleY = canvas.height / originalRes[1];
|
| 498 |
+
|
| 499 |
+
const point = candidates[keypointIndex];
|
| 500 |
+
if (point) {
|
| 501 |
+
const keypointX = point[0] * scaleX;
|
| 502 |
+
const keypointY = point[1] * scaleY;
|
| 503 |
+
|
| 504 |
+
dragOffset = {
|
| 505 |
+
x: mousePos.x - keypointX,
|
| 506 |
+
y: mousePos.y - keypointY
|
| 507 |
+
};
|
| 508 |
+
}
|
| 509 |
+
}
|
| 510 |
+
|
| 511 |
+
debugLog(`Drag started: keypoint ${keypointIndex} at (${mousePos.x}, ${mousePos.y})`);
|
| 512 |
+
canvas.style.cursor = 'grabbing';
|
| 513 |
+
} else {
|
| 514 |
+
debugLog("No keypoint found near mouse position");
|
| 515 |
+
}
|
| 516 |
+
}
|
| 517 |
+
}
|
| 518 |
+
}
|
| 519 |
+
|
| 520 |
+
// 🎯 詳細モード用:手・顔・ボディキーポイント位置更新
|
| 521 |
+
function updateDetailKeypointPosition(detailKeypoint, canvasX, canvasY) {
|
| 522 |
+
const currentPoseData = window.poseEditorGlobals.poseData || poseData;
|
| 523 |
+
|
| 524 |
+
if (!currentPoseData) {
|
| 525 |
+
debugLog("No poseData available for detail keypoint update");
|
| 526 |
+
return;
|
| 527 |
+
}
|
| 528 |
+
|
| 529 |
+
const originalRes = currentPoseData.resolution || [512, 512];
|
| 530 |
+
const scaleX = canvas.width / originalRes[0];
|
| 531 |
+
const scaleY = canvas.height / originalRes[1];
|
| 532 |
+
|
| 533 |
+
// Canvas座標をデータ座標に変換
|
| 534 |
+
const dataX = Math.max(0, Math.min(originalRes[0], canvasX / scaleX));
|
| 535 |
+
const dataY = Math.max(0, Math.min(originalRes[1], canvasY / scaleY));
|
| 536 |
+
|
| 537 |
+
switch (detailKeypoint.type) {
|
| 538 |
+
case 'hand':
|
| 539 |
+
// 手のキーポイント更新
|
| 540 |
+
if (currentPoseData.people && currentPoseData.people[0]) {
|
| 541 |
+
const handDataKey = detailKeypoint.handType === 'left' ? 'hand_left_keypoints_2d' : 'hand_right_keypoints_2d';
|
| 542 |
+
|
| 543 |
+
if (!currentPoseData.people[0][handDataKey]) {
|
| 544 |
+
// 💖 people形式で編集済みデータが存在しない場合は作成
|
| 545 |
+
let originalHandData = null;
|
| 546 |
+
if (currentPoseData.people && currentPoseData.people[0]) {
|
| 547 |
+
if (detailKeypoint.handIndex === 0) {
|
| 548 |
+
originalHandData = currentPoseData.people[0].hand_left_keypoints_2d;
|
| 549 |
+
} else if (detailKeypoint.handIndex === 1) {
|
| 550 |
+
originalHandData = currentPoseData.people[0].hand_right_keypoints_2d;
|
| 551 |
+
}
|
| 552 |
+
}
|
| 553 |
+
if (originalHandData && !currentPoseData.people[0][handDataKey]) {
|
| 554 |
+
currentPoseData.people[0][handDataKey] = [...originalHandData];
|
| 555 |
+
}
|
| 556 |
+
}
|
| 557 |
+
|
| 558 |
+
if (currentPoseData.people[0][handDataKey]) {
|
| 559 |
+
const handData = currentPoseData.people[0][handDataKey];
|
| 560 |
+
const arrayIndex = detailKeypoint.arrayIndex;
|
| 561 |
+
|
| 562 |
+
if (arrayIndex < handData.length - 1) {
|
| 563 |
+
handData[arrayIndex] = dataX;
|
| 564 |
+
handData[arrayIndex + 1] = dataY;
|
| 565 |
+
// 信頼度は維持
|
| 566 |
+
debugLog(`🫳 Hand ${detailKeypoint.handType} keypoint ${detailKeypoint.keypointIndex} updated to (${dataX}, ${dataY})`);
|
| 567 |
+
|
| 568 |
+
// 🚀 元のhandsデータも同期更新
|
| 569 |
+
syncHandsToOriginal(currentPoseData, detailKeypoint.handType, handData);
|
| 570 |
+
}
|
| 571 |
+
}
|
| 572 |
+
}
|
| 573 |
+
break;
|
| 574 |
+
|
| 575 |
+
case 'face':
|
| 576 |
+
// 顔のキーポイント更新
|
| 577 |
+
if (currentPoseData.people && currentPoseData.people[0]) {
|
| 578 |
+
if (!currentPoseData.people[0].face_keypoints_2d) {
|
| 579 |
+
// 編集済みデータが存在しない場合は作成
|
| 580 |
+
const originalFaceData = currentPoseData.faces && currentPoseData.faces[0];
|
| 581 |
+
if (originalFaceData) {
|
| 582 |
+
currentPoseData.people[0].face_keypoints_2d = [...originalFaceData];
|
| 583 |
+
}
|
| 584 |
+
}
|
| 585 |
+
|
| 586 |
+
if (currentPoseData.people[0].face_keypoints_2d) {
|
| 587 |
+
const faceData = currentPoseData.people[0].face_keypoints_2d;
|
| 588 |
+
const arrayIndex = detailKeypoint.arrayIndex;
|
| 589 |
+
|
| 590 |
+
if (arrayIndex < faceData.length - 1) {
|
| 591 |
+
faceData[arrayIndex] = dataX;
|
| 592 |
+
faceData[arrayIndex + 1] = dataY;
|
| 593 |
+
// 信頼度は維持
|
| 594 |
+
debugLog(`😊 Face keypoint ${detailKeypoint.keypointIndex} updated to (${dataX}, ${dataY})`);
|
| 595 |
+
|
| 596 |
+
// 🚀 元のfacesデータも同期更新
|
| 597 |
+
syncFacesToOriginal(currentPoseData, faceData);
|
| 598 |
+
}
|
| 599 |
+
}
|
| 600 |
+
}
|
| 601 |
+
break;
|
| 602 |
+
|
| 603 |
+
case 'body':
|
| 604 |
+
// ボディキーポイント更新(既存の関数を使用)
|
| 605 |
+
updateKeypointPosition(detailKeypoint.keypointIndex, canvasX, canvasY);
|
| 606 |
+
break;
|
| 607 |
+
}
|
| 608 |
+
}
|
| 609 |
+
|
| 610 |
+
// 🚀 手のデータ同期関数
|
| 611 |
+
function syncHandsToOriginal(poseData, handType, handData) {
|
| 612 |
+
if (!poseData.hands) return;
|
| 613 |
+
|
| 614 |
+
const handIndex = handType === 'left' ? 0 : 1;
|
| 615 |
+
if (handIndex < poseData.hands.length) {
|
| 616 |
+
poseData.hands[handIndex] = [...handData];
|
| 617 |
+
debugLog(`🔄 Hand ${handType} synced to original hands data`);
|
| 618 |
+
}
|
| 619 |
+
}
|
| 620 |
+
|
| 621 |
+
// 🚀 顔のデータ同期関数
|
| 622 |
+
function syncFacesToOriginal(poseData, faceData) {
|
| 623 |
+
if (!poseData.faces) return;
|
| 624 |
+
|
| 625 |
+
if (poseData.faces.length > 0) {
|
| 626 |
+
poseData.faces[0] = [...faceData];
|
| 627 |
+
debugLog(`🔄 Face synced to original faces data`);
|
| 628 |
+
}
|
| 629 |
+
}
|
| 630 |
+
|
| 631 |
+
// 🎯 矩形変形によるキーポイント一括変換機能(refs互換・Issue 028実装)
|
| 632 |
+
function transformKeypointsInRect(control, newMouseX, newMouseY) {
|
| 633 |
+
// 1. データとコントロールの存在確認
|
| 634 |
+
if (!window.poseEditorGlobals.poseData || !control) {
|
| 635 |
+
return;
|
| 636 |
+
}
|
| 637 |
+
|
| 638 |
+
// 2. people形式データの確認
|
| 639 |
+
if (!window.poseEditorGlobals.poseData.people ||
|
| 640 |
+
!Array.isArray(window.poseEditorGlobals.poseData.people) ||
|
| 641 |
+
window.poseEditorGlobals.poseData.people.length === 0) {
|
| 642 |
+
return;
|
| 643 |
+
}
|
| 644 |
+
|
| 645 |
+
const person = window.poseEditorGlobals.poseData.people[0];
|
| 646 |
+
|
| 647 |
+
// 3. 対象キーポイントデータを取得
|
| 648 |
+
let targetKeypoints;
|
| 649 |
+
let fieldName;
|
| 650 |
+
|
| 651 |
+
switch (control.type) {
|
| 652 |
+
case 'face':
|
| 653 |
+
targetKeypoints = person.face_keypoints_2d;
|
| 654 |
+
fieldName = 'face_keypoints_2d';
|
| 655 |
+
break;
|
| 656 |
+
case 'leftHand':
|
| 657 |
+
targetKeypoints = person.hand_left_keypoints_2d;
|
| 658 |
+
fieldName = 'hand_left_keypoints_2d';
|
| 659 |
+
break;
|
| 660 |
+
case 'rightHand':
|
| 661 |
+
targetKeypoints = person.hand_right_keypoints_2d;
|
| 662 |
+
fieldName = 'hand_right_keypoints_2d';
|
| 663 |
+
break;
|
| 664 |
+
default:
|
| 665 |
+
return;
|
| 666 |
+
}
|
| 667 |
+
|
| 668 |
+
if (!targetKeypoints || !Array.isArray(targetKeypoints)) {
|
| 669 |
+
return;
|
| 670 |
+
}
|
| 671 |
+
|
| 672 |
+
// 4. 元の矩形サイズを取得
|
| 673 |
+
const originalRect = control.rect;
|
| 674 |
+
|
| 675 |
+
// 5. 新しい矩形サイズを計算(角に応じて)
|
| 676 |
+
let newRect = { ...originalRect };
|
| 677 |
+
|
| 678 |
+
switch (control.corner) {
|
| 679 |
+
case 'TL': // 左上
|
| 680 |
+
newRect.width = originalRect.width + (originalRect.x - newMouseX);
|
| 681 |
+
newRect.height = originalRect.height + (originalRect.y - newMouseY);
|
| 682 |
+
newRect.x = newMouseX;
|
| 683 |
+
newRect.y = newMouseY;
|
| 684 |
+
break;
|
| 685 |
+
case 'TR': // 右上
|
| 686 |
+
newRect.width = newMouseX - originalRect.x;
|
| 687 |
+
newRect.height = originalRect.height + (originalRect.y - newMouseY);
|
| 688 |
+
newRect.y = newMouseY;
|
| 689 |
+
break;
|
| 690 |
+
case 'BL': // 左下
|
| 691 |
+
newRect.width = originalRect.width + (originalRect.x - newMouseX);
|
| 692 |
+
newRect.height = newMouseY - originalRect.y;
|
| 693 |
+
newRect.x = newMouseX;
|
| 694 |
+
break;
|
| 695 |
+
case 'BR': // 右下
|
| 696 |
+
newRect.width = newMouseX - originalRect.x;
|
| 697 |
+
newRect.height = newMouseY - originalRect.y;
|
| 698 |
+
break;
|
| 699 |
+
}
|
| 700 |
+
|
| 701 |
+
// 6. 最小サイズ制限
|
| 702 |
+
const minSize = 20;
|
| 703 |
+
if (newRect.width < minSize || newRect.height < minSize) return;
|
| 704 |
+
|
| 705 |
+
// 7. 変換比率を計算
|
| 706 |
+
const scaleX = newRect.width / originalRect.width;
|
| 707 |
+
const scaleY = newRect.height / originalRect.height;
|
| 708 |
+
|
| 709 |
+
// 8. 座標変換設定の準備
|
| 710 |
+
const canvasWidth = window.poseEditorGlobals.canvas ? window.poseEditorGlobals.canvas.width : 512;
|
| 711 |
+
const canvasHeight = window.poseEditorGlobals.canvas ? window.poseEditorGlobals.canvas.height : 512;
|
| 712 |
+
|
| 713 |
+
let dataResolutionWidth = canvasWidth;
|
| 714 |
+
let dataResolutionHeight = canvasHeight;
|
| 715 |
+
|
| 716 |
+
// 解像度情報の取得(現在のデータ構造に対応)
|
| 717 |
+
const currentPoseData = window.poseEditorGlobals.poseData || poseData;
|
| 718 |
+
if (currentPoseData.resolution && Array.isArray(currentPoseData.resolution) && currentPoseData.resolution.length >= 2) {
|
| 719 |
+
dataResolutionWidth = currentPoseData.resolution[0];
|
| 720 |
+
dataResolutionHeight = currentPoseData.resolution[1];
|
| 721 |
+
}
|
| 722 |
+
|
| 723 |
+
const coordScaleX = canvasWidth / dataResolutionWidth;
|
| 724 |
+
const coordScaleY = canvasHeight / dataResolutionHeight;
|
| 725 |
+
|
| 726 |
+
// 9. 正規化座標かピクセル座標かを判定
|
| 727 |
+
let isNormalized = false;
|
| 728 |
+
if (targetKeypoints.length > 0) {
|
| 729 |
+
// 最初の有効なキーポイントで判定
|
| 730 |
+
for (let i = 0; i < targetKeypoints.length; i += 3) {
|
| 731 |
+
if (i + 2 < targetKeypoints.length && targetKeypoints[i + 2] > 0) {
|
| 732 |
+
const x = targetKeypoints[i];
|
| 733 |
+
const y = targetKeypoints[i + 1];
|
| 734 |
+
isNormalized = (x >= 0 && x <= 1 && y >= 0 && y <= 1);
|
| 735 |
+
break;
|
| 736 |
+
}
|
| 737 |
+
}
|
| 738 |
+
}
|
| 739 |
+
|
| 740 |
+
// 10. 対象キーポイントを一括変換
|
| 741 |
+
for (let i = 0; i < targetKeypoints.length; i += 3) {
|
| 742 |
+
if (i + 2 < targetKeypoints.length) {
|
| 743 |
+
const confidence = targetKeypoints[i + 2];
|
| 744 |
+
if (confidence > 0.1) { // 閾値を下げて、より多くのポイントを変換
|
| 745 |
+
let x = targetKeypoints[i];
|
| 746 |
+
let y = targetKeypoints[i + 1];
|
| 747 |
+
|
| 748 |
+
// データ座標→Canvas座標
|
| 749 |
+
let canvasX, canvasY;
|
| 750 |
+
if (isNormalized) {
|
| 751 |
+
canvasX = (x * dataResolutionWidth) * coordScaleX;
|
| 752 |
+
canvasY = (y * dataResolutionHeight) * coordScaleY;
|
| 753 |
+
} else {
|
| 754 |
+
canvasX = x * coordScaleX;
|
| 755 |
+
canvasY = y * coordScaleY;
|
| 756 |
+
}
|
| 757 |
+
|
| 758 |
+
// 元矩形内での相対位置を計算
|
| 759 |
+
const relativeX = (canvasX - originalRect.x) / originalRect.width;
|
| 760 |
+
const relativeY = (canvasY - originalRect.y) / originalRect.height;
|
| 761 |
+
|
| 762 |
+
// 新矩形での新しい位置を計算
|
| 763 |
+
const newCanvasX = newRect.x + (relativeX * newRect.width);
|
| 764 |
+
const newCanvasY = newRect.y + (relativeY * newRect.height);
|
| 765 |
+
|
| 766 |
+
// Canvas座標→データ座標に戻す
|
| 767 |
+
if (isNormalized) {
|
| 768 |
+
const dataX = newCanvasX / coordScaleX;
|
| 769 |
+
const dataY = newCanvasY / coordScaleY;
|
| 770 |
+
targetKeypoints[i] = dataX / dataResolutionWidth;
|
| 771 |
+
targetKeypoints[i + 1] = dataY / dataResolutionHeight;
|
| 772 |
+
} else {
|
| 773 |
+
targetKeypoints[i] = newCanvasX / coordScaleX;
|
| 774 |
+
targetKeypoints[i + 1] = newCanvasY / coordScaleY;
|
| 775 |
+
}
|
| 776 |
+
}
|
| 777 |
+
}
|
| 778 |
+
}
|
| 779 |
+
|
| 780 |
+
// 11. 矩形情報を更新
|
| 781 |
+
control.rect = newRect;
|
| 782 |
+
|
| 783 |
+
debugLog(`🎯 Transformed ${fieldName} keypoints: scale=(${scaleX.toFixed(2)}, ${scaleY.toFixed(2)})`);
|
| 784 |
+
}
|
| 785 |
+
|
| 786 |
+
// 🔧 直接矩形変換関数(シンプル版)
|
| 787 |
+
function transformKeypointsDirectly(rectType, originalRect, newRect) {
|
| 788 |
+
const currentPoseData = window.poseEditorGlobals.poseData || poseData;
|
| 789 |
+
const originalKeypoints = window.poseEditorGlobals.originalKeypoints;
|
| 790 |
+
|
| 791 |
+
if (!currentPoseData || !currentPoseData.people || !currentPoseData.people[0]) {
|
| 792 |
+
debugLog("❌ transformKeypointsDirectly: No pose data available");
|
| 793 |
+
return;
|
| 794 |
+
}
|
| 795 |
+
|
| 796 |
+
if (!originalKeypoints || !originalKeypoints.people || !originalKeypoints.people[0]) {
|
| 797 |
+
debugLog("❌ transformKeypointsDirectly: No original keypoints available");
|
| 798 |
+
return;
|
| 799 |
+
}
|
| 800 |
+
|
| 801 |
+
const person = currentPoseData.people[0];
|
| 802 |
+
const originalPerson = originalKeypoints.people[0];
|
| 803 |
+
|
| 804 |
+
// 対象キーポイントデータを取得(元データと現在データ両方)
|
| 805 |
+
let targetKeypoints, originalTargetKeypoints;
|
| 806 |
+
let fieldName;
|
| 807 |
+
|
| 808 |
+
switch (rectType) {
|
| 809 |
+
case 'face':
|
| 810 |
+
targetKeypoints = person.face_keypoints_2d;
|
| 811 |
+
originalTargetKeypoints = originalPerson.face_keypoints_2d;
|
| 812 |
+
fieldName = 'face_keypoints_2d';
|
| 813 |
+
break;
|
| 814 |
+
case 'leftHand':
|
| 815 |
+
targetKeypoints = person.hand_left_keypoints_2d;
|
| 816 |
+
originalTargetKeypoints = originalPerson.hand_left_keypoints_2d;
|
| 817 |
+
fieldName = 'hand_left_keypoints_2d';
|
| 818 |
+
break;
|
| 819 |
+
case 'rightHand':
|
| 820 |
+
targetKeypoints = person.hand_right_keypoints_2d;
|
| 821 |
+
originalTargetKeypoints = originalPerson.hand_right_keypoints_2d;
|
| 822 |
+
fieldName = 'hand_right_keypoints_2d';
|
| 823 |
+
break;
|
| 824 |
+
default:
|
| 825 |
+
debugLog(`❌ transformKeypointsDirectly: Unknown rectType ${rectType}`);
|
| 826 |
+
return;
|
| 827 |
+
}
|
| 828 |
+
|
| 829 |
+
if (!targetKeypoints || !Array.isArray(targetKeypoints)) {
|
| 830 |
+
debugLog(`❌ transformKeypointsDirectly: No keypoints for ${fieldName}`);
|
| 831 |
+
debugLog(`🔍 Person keys: ${Object.keys(person)}`);
|
| 832 |
+
return;
|
| 833 |
+
}
|
| 834 |
+
|
| 835 |
+
if (!originalTargetKeypoints || !Array.isArray(originalTargetKeypoints)) {
|
| 836 |
+
debugLog(`❌ transformKeypointsDirectly: No original keypoints for ${fieldName}`);
|
| 837 |
+
debugLog(`🔍 Original person keys: ${Object.keys(originalPerson)}`);
|
| 838 |
+
return;
|
| 839 |
+
}
|
| 840 |
+
|
| 841 |
+
debugLog(`✅ transformKeypointsDirectly: Found ${fieldName} - current: ${targetKeypoints.length}, original: ${originalTargetKeypoints.length}`);
|
| 842 |
+
|
| 843 |
+
// 座標変換設定
|
| 844 |
+
const canvasWidth = window.poseEditorGlobals.canvas ? window.poseEditorGlobals.canvas.width : 512;
|
| 845 |
+
const canvasHeight = window.poseEditorGlobals.canvas ? window.poseEditorGlobals.canvas.height : 512;
|
| 846 |
+
|
| 847 |
+
let dataResolutionWidth = canvasWidth;
|
| 848 |
+
let dataResolutionHeight = canvasHeight;
|
| 849 |
+
|
| 850 |
+
// 解像度情報の取得
|
| 851 |
+
if (currentPoseData.resolution && Array.isArray(currentPoseData.resolution) && currentPoseData.resolution.length >= 2) {
|
| 852 |
+
dataResolutionWidth = currentPoseData.resolution[0];
|
| 853 |
+
dataResolutionHeight = currentPoseData.resolution[1];
|
| 854 |
+
}
|
| 855 |
+
|
| 856 |
+
const coordScaleX = canvasWidth / dataResolutionWidth;
|
| 857 |
+
const coordScaleY = canvasHeight / dataResolutionHeight;
|
| 858 |
+
|
| 859 |
+
// 正規化座標かピクセル座標かを判定
|
| 860 |
+
let isNormalized = false;
|
| 861 |
+
if (targetKeypoints.length > 0) {
|
| 862 |
+
for (let i = 0; i < targetKeypoints.length; i += 3) {
|
| 863 |
+
if (i + 2 < targetKeypoints.length && targetKeypoints[i + 2] > 0) {
|
| 864 |
+
const x = targetKeypoints[i];
|
| 865 |
+
const y = targetKeypoints[i + 1];
|
| 866 |
+
isNormalized = (x >= 0 && x <= 1 && y >= 0 && y <= 1);
|
| 867 |
+
break;
|
| 868 |
+
}
|
| 869 |
+
}
|
| 870 |
+
}
|
| 871 |
+
|
| 872 |
+
debugLog(`🔧 transformKeypointsDirectly: ${fieldName}, original rect: ${JSON.stringify(originalRect)}, new rect: ${JSON.stringify(newRect)}, normalized: ${isNormalized}`);
|
| 873 |
+
|
| 874 |
+
// キーポイントを一括変換(元データから毎回変換)
|
| 875 |
+
for (let i = 0; i < originalTargetKeypoints.length; i += 3) {
|
| 876 |
+
if (i + 2 < originalTargetKeypoints.length && i + 2 < targetKeypoints.length) {
|
| 877 |
+
const confidence = originalTargetKeypoints[i + 2];
|
| 878 |
+
if (confidence > 0.1) {
|
| 879 |
+
// 🎯 元データから取得(累積変形防止)
|
| 880 |
+
let x = originalTargetKeypoints[i];
|
| 881 |
+
let y = originalTargetKeypoints[i + 1];
|
| 882 |
+
|
| 883 |
+
// データ座標→Canvas座標
|
| 884 |
+
let canvasX, canvasY;
|
| 885 |
+
if (isNormalized) {
|
| 886 |
+
canvasX = (x * dataResolutionWidth) * coordScaleX;
|
| 887 |
+
canvasY = (y * dataResolutionHeight) * coordScaleY;
|
| 888 |
+
} else {
|
| 889 |
+
canvasX = x * coordScaleX;
|
| 890 |
+
canvasY = y * coordScaleY;
|
| 891 |
+
}
|
| 892 |
+
|
| 893 |
+
// 元矩形内での相対位置を計算
|
| 894 |
+
const relativeX = (canvasX - originalRect.x) / originalRect.width;
|
| 895 |
+
const relativeY = (canvasY - originalRect.y) / originalRect.height;
|
| 896 |
+
|
| 897 |
+
// 新矩形での新しい位置を計算
|
| 898 |
+
const newCanvasX = newRect.x + (relativeX * newRect.width);
|
| 899 |
+
const newCanvasY = newRect.y + (relativeY * newRect.height);
|
| 900 |
+
|
| 901 |
+
// Canvas座標→データ座標に戻す
|
| 902 |
+
if (isNormalized) {
|
| 903 |
+
const dataX = newCanvasX / coordScaleX;
|
| 904 |
+
const dataY = newCanvasY / coordScaleY;
|
| 905 |
+
targetKeypoints[i] = dataX / dataResolutionWidth;
|
| 906 |
+
targetKeypoints[i + 1] = dataY / dataResolutionHeight;
|
| 907 |
+
} else {
|
| 908 |
+
targetKeypoints[i] = newCanvasX / coordScaleX;
|
| 909 |
+
targetKeypoints[i + 1] = newCanvasY / coordScaleY;
|
| 910 |
+
}
|
| 911 |
+
}
|
| 912 |
+
}
|
| 913 |
+
}
|
| 914 |
+
|
| 915 |
+
debugLog(`✅ transformKeypointsDirectly: Transformed ${fieldName} keypoints successfully`);
|
| 916 |
+
}
|
| 917 |
+
|
| 918 |
+
// 🔧 元座標からキーポイントを移動(累積移動防止版)
|
| 919 |
+
function moveKeypointsFromOriginal(rectType, totalDeltaX, totalDeltaY) {
|
| 920 |
+
console.log('🔍 [moveKeypointsFromOriginal] Called with:', {
|
| 921 |
+
rectType,
|
| 922 |
+
totalDeltaX,
|
| 923 |
+
totalDeltaY,
|
| 924 |
+
hasOriginalKeypoints: !!window.poseEditorGlobals.originalKeypoints,
|
| 925 |
+
hasBaseOriginalKeypoints: !!window.poseEditorGlobals.baseOriginalKeypoints
|
| 926 |
+
});
|
| 927 |
+
|
| 928 |
+
const currentPoseData = window.poseEditorGlobals.poseData || poseData;
|
| 929 |
+
const originalKeypoints = window.poseEditorGlobals.originalKeypoints;
|
| 930 |
+
|
| 931 |
+
if (!currentPoseData || !currentPoseData.people || !currentPoseData.people[0]) {
|
| 932 |
+
debugLog("❌ moveKeypointsFromOriginal: No current pose data available");
|
| 933 |
+
return;
|
| 934 |
+
}
|
| 935 |
+
|
| 936 |
+
if (!originalKeypoints || !originalKeypoints.people || !originalKeypoints.people[0]) {
|
| 937 |
+
debugLog("❌ moveKeypointsFromOriginal: No original keypoints available");
|
| 938 |
+
return;
|
| 939 |
+
}
|
| 940 |
+
|
| 941 |
+
const person = currentPoseData.people[0];
|
| 942 |
+
const originalPerson = originalKeypoints.people[0];
|
| 943 |
+
|
| 944 |
+
// 対象キーポイントデータを取得
|
| 945 |
+
let targetKeypoints, originalTargetKeypoints;
|
| 946 |
+
let fieldName;
|
| 947 |
+
|
| 948 |
+
switch (rectType) {
|
| 949 |
+
case 'face':
|
| 950 |
+
targetKeypoints = person.face_keypoints_2d;
|
| 951 |
+
originalTargetKeypoints = originalPerson.face_keypoints_2d;
|
| 952 |
+
fieldName = 'face_keypoints_2d';
|
| 953 |
+
break;
|
| 954 |
+
case 'leftHand':
|
| 955 |
+
targetKeypoints = person.hand_left_keypoints_2d;
|
| 956 |
+
originalTargetKeypoints = originalPerson.hand_left_keypoints_2d;
|
| 957 |
+
fieldName = 'hand_left_keypoints_2d';
|
| 958 |
+
break;
|
| 959 |
+
case 'rightHand':
|
| 960 |
+
targetKeypoints = person.hand_right_keypoints_2d;
|
| 961 |
+
originalTargetKeypoints = originalPerson.hand_right_keypoints_2d;
|
| 962 |
+
fieldName = 'hand_right_keypoints_2d';
|
| 963 |
+
break;
|
| 964 |
+
default:
|
| 965 |
+
debugLog(`❌ moveKeypointsFromOriginal: Unknown rectType ${rectType}`);
|
| 966 |
+
return;
|
| 967 |
+
}
|
| 968 |
+
|
| 969 |
+
if (!targetKeypoints || !Array.isArray(targetKeypoints)) {
|
| 970 |
+
debugLog(`❌ moveKeypointsFromOriginal: No current keypoints for ${fieldName}`);
|
| 971 |
+
return;
|
| 972 |
+
}
|
| 973 |
+
|
| 974 |
+
if (!originalTargetKeypoints || !Array.isArray(originalTargetKeypoints)) {
|
| 975 |
+
debugLog(`❌ moveKeypointsFromOriginal: No original keypoints for ${fieldName}`);
|
| 976 |
+
return;
|
| 977 |
+
}
|
| 978 |
+
|
| 979 |
+
// 座標変換設定
|
| 980 |
+
const canvasWidth = window.poseEditorGlobals.canvas ? window.poseEditorGlobals.canvas.width : 512;
|
| 981 |
+
const canvasHeight = window.poseEditorGlobals.canvas ? window.poseEditorGlobals.canvas.height : 512;
|
| 982 |
+
|
| 983 |
+
let dataResolutionWidth = canvasWidth;
|
| 984 |
+
let dataResolutionHeight = canvasHeight;
|
| 985 |
+
|
| 986 |
+
// 解像度情報の取得
|
| 987 |
+
if (currentPoseData.resolution && Array.isArray(currentPoseData.resolution) && currentPoseData.resolution.length >= 2) {
|
| 988 |
+
dataResolutionWidth = currentPoseData.resolution[0];
|
| 989 |
+
dataResolutionHeight = currentPoseData.resolution[1];
|
| 990 |
+
}
|
| 991 |
+
|
| 992 |
+
const coordScaleX = canvasWidth / dataResolutionWidth;
|
| 993 |
+
const coordScaleY = canvasHeight / dataResolutionHeight;
|
| 994 |
+
|
| 995 |
+
// 正規化座標かピクセル座標かを判定
|
| 996 |
+
let isNormalized = false;
|
| 997 |
+
if (originalTargetKeypoints.length > 0) {
|
| 998 |
+
for (let i = 0; i < originalTargetKeypoints.length; i += 3) {
|
| 999 |
+
if (i + 2 < originalTargetKeypoints.length && originalTargetKeypoints[i + 2] > 0) {
|
| 1000 |
+
const x = originalTargetKeypoints[i];
|
| 1001 |
+
const y = originalTargetKeypoints[i + 1];
|
| 1002 |
+
isNormalized = (x >= 0 && x <= 1 && y >= 0 && y <= 1);
|
| 1003 |
+
break;
|
| 1004 |
+
}
|
| 1005 |
+
}
|
| 1006 |
+
}
|
| 1007 |
+
|
| 1008 |
+
debugLog(`🔧 moveKeypointsFromOriginal: ${fieldName}, delta=(${totalDeltaX}, ${totalDeltaY}), normalized: ${isNormalized}`);
|
| 1009 |
+
|
| 1010 |
+
// キーポイントを移動(元データから移動量を適用)
|
| 1011 |
+
for (let i = 0; i < originalTargetKeypoints.length; i += 3) {
|
| 1012 |
+
if (i + 2 < originalTargetKeypoints.length && i + 2 < targetKeypoints.length) {
|
| 1013 |
+
const confidence = originalTargetKeypoints[i + 2];
|
| 1014 |
+
if (confidence > 0.1) {
|
| 1015 |
+
// 元データから取得
|
| 1016 |
+
let origX = originalTargetKeypoints[i];
|
| 1017 |
+
let origY = originalTargetKeypoints[i + 1];
|
| 1018 |
+
|
| 1019 |
+
// データ座標→Canvas座標
|
| 1020 |
+
let canvasX, canvasY;
|
| 1021 |
+
if (isNormalized) {
|
| 1022 |
+
canvasX = (origX * dataResolutionWidth) * coordScaleX;
|
| 1023 |
+
canvasY = (origY * dataResolutionHeight) * coordScaleY;
|
| 1024 |
+
} else {
|
| 1025 |
+
canvasX = origX * coordScaleX;
|
| 1026 |
+
canvasY = origY * coordScaleY;
|
| 1027 |
+
}
|
| 1028 |
+
|
| 1029 |
+
// 移動量を適用
|
| 1030 |
+
const newCanvasX = canvasX + totalDeltaX;
|
| 1031 |
+
const newCanvasY = canvasY + totalDeltaY;
|
| 1032 |
+
|
| 1033 |
+
// Canvas座標→データ座標に戻す
|
| 1034 |
+
if (isNormalized) {
|
| 1035 |
+
const dataX = newCanvasX / coordScaleX;
|
| 1036 |
+
const dataY = newCanvasY / coordScaleY;
|
| 1037 |
+
targetKeypoints[i] = dataX / dataResolutionWidth;
|
| 1038 |
+
targetKeypoints[i + 1] = dataY / dataResolutionHeight;
|
| 1039 |
+
} else {
|
| 1040 |
+
targetKeypoints[i] = newCanvasX / coordScaleX;
|
| 1041 |
+
targetKeypoints[i + 1] = newCanvasY / coordScaleY;
|
| 1042 |
+
}
|
| 1043 |
+
}
|
| 1044 |
+
}
|
| 1045 |
+
}
|
| 1046 |
+
|
| 1047 |
+
debugLog(`✅ moveKeypointsFromOriginal: Moved ${fieldName} keypoints successfully`);
|
| 1048 |
+
}
|
| 1049 |
+
|
| 1050 |
+
// 🔧 矩形移動に合わせてキーポイントを移動(refs互換版)
|
| 1051 |
+
function moveKeypointsWithRect(rectType, deltaX, deltaY) {
|
| 1052 |
+
const currentPoseData = window.poseEditorGlobals.poseData || poseData;
|
| 1053 |
+
if (!currentPoseData || !currentPoseData.people || !currentPoseData.people[0]) {
|
| 1054 |
+
debugLog("❌ moveKeypointsWithRect: No current pose data available");
|
| 1055 |
+
return;
|
| 1056 |
+
}
|
| 1057 |
+
|
| 1058 |
+
const person = currentPoseData.people[0];
|
| 1059 |
+
|
| 1060 |
+
debugLog(`🔧 キーポイント移動: rectType=${rectType}, delta=(${deltaX}, ${deltaY})`);
|
| 1061 |
+
|
| 1062 |
+
// 対応するキーポイントを取得
|
| 1063 |
+
let keypoints = null;
|
| 1064 |
+
let fieldName = '';
|
| 1065 |
+
|
| 1066 |
+
switch (rectType) {
|
| 1067 |
+
case 'face':
|
| 1068 |
+
keypoints = person.face_keypoints_2d;
|
| 1069 |
+
fieldName = 'face_keypoints_2d';
|
| 1070 |
+
break;
|
| 1071 |
+
case 'leftHand':
|
| 1072 |
+
keypoints = person.hand_left_keypoints_2d;
|
| 1073 |
+
fieldName = 'hand_left_keypoints_2d';
|
| 1074 |
+
break;
|
| 1075 |
+
case 'rightHand':
|
| 1076 |
+
keypoints = person.hand_right_keypoints_2d;
|
| 1077 |
+
fieldName = 'hand_right_keypoints_2d';
|
| 1078 |
+
break;
|
| 1079 |
+
default:
|
| 1080 |
+
debugLog(`❌ moveKeypointsWithRect: Unknown rectType ${rectType}`);
|
| 1081 |
+
return;
|
| 1082 |
+
}
|
| 1083 |
+
|
| 1084 |
+
if (!keypoints || !Array.isArray(keypoints)) {
|
| 1085 |
+
debugLog(`❌ moveKeypointsWithRect: No keypoints for ${fieldName}`);
|
| 1086 |
+
return;
|
| 1087 |
+
}
|
| 1088 |
+
|
| 1089 |
+
// すべてのキーポイントを移動(refs方式)
|
| 1090 |
+
for (let i = 0; i < keypoints.length; i += 3) {
|
| 1091 |
+
const confidence = keypoints[i + 2];
|
| 1092 |
+
|
| 1093 |
+
if (confidence > 0.1) { // 有効なキーポイントのみ移動
|
| 1094 |
+
keypoints[i] += deltaX; // X座標
|
| 1095 |
+
keypoints[i + 1] += deltaY; // Y座標
|
| 1096 |
+
}
|
| 1097 |
+
}
|
| 1098 |
+
|
| 1099 |
+
// データを更新
|
| 1100 |
+
person[fieldName] = keypoints;
|
| 1101 |
+
|
| 1102 |
+
debugLog(`✅ moveKeypointsWithRect: Moved ${fieldName} keypoints successfully`);
|
| 1103 |
+
}
|
| 1104 |
+
|
| 1105 |
+
// 🚀 全データをエクスポート用フォーマットに同期(refs互換)
|
| 1106 |
+
function syncAllDataToExportFormat(poseData) {
|
| 1107 |
+
if (!poseData) return;
|
| 1108 |
+
|
| 1109 |
+
// 1. bodies.candidate → people.pose_keypoints_2d 同期
|
| 1110 |
+
syncBodiesToPeople(poseData);
|
| 1111 |
+
|
| 1112 |
+
// 2. 手の編集データがあれば元データにも同期
|
| 1113 |
+
if (poseData.people && poseData.people[0]) {
|
| 1114 |
+
const person = poseData.people[0];
|
| 1115 |
|
| 1116 |
+
// 左手同期
|
| 1117 |
+
if (person.hand_left_keypoints_2d) {
|
| 1118 |
+
syncHandsToOriginal(poseData, 'left', person.hand_left_keypoints_2d);
|
| 1119 |
+
}
|
| 1120 |
|
| 1121 |
+
// 右手同期
|
| 1122 |
+
if (person.hand_right_keypoints_2d) {
|
| 1123 |
+
syncHandsToOriginal(poseData, 'right', person.hand_right_keypoints_2d);
|
| 1124 |
+
}
|
| 1125 |
+
|
| 1126 |
+
// 顔同期
|
| 1127 |
+
if (person.face_keypoints_2d) {
|
| 1128 |
+
syncFacesToOriginal(poseData, person.face_keypoints_2d);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1129 |
}
|
| 1130 |
}
|
| 1131 |
+
|
| 1132 |
+
debugLog(`🔄 All data synced to export format - People: ${poseData.people ? poseData.people.length : 0}`);
|
| 1133 |
}
|
| 1134 |
|
| 1135 |
// マウス移動処理(refs互換 + 矩形編集対応)
|
|
|
|
| 1151 |
return;
|
| 1152 |
}
|
| 1153 |
|
| 1154 |
+
// 🎯 詳細モードの手・顔・ボディキーポイントドラッグ処理
|
| 1155 |
+
if (window.poseEditorGlobals.draggedDetailKeypoint) {
|
| 1156 |
+
updateDetailKeypointPosition(window.poseEditorGlobals.draggedDetailKeypoint, mousePos.x, mousePos.y);
|
| 1157 |
+
|
| 1158 |
+
// リアルタイム再描画(エラーハンドリング付き・手顔データ保持強化)
|
| 1159 |
+
try {
|
| 1160 |
+
const currentPoseData = window.poseEditorGlobals.poseData || poseData;
|
| 1161 |
+
if (currentPoseData) {
|
| 1162 |
+
// 🫳😊 手と顔データが存在することを確認してから描画
|
| 1163 |
+
debugLog(`🎨 Real-time redraw: hands=${!!(currentPoseData.people && currentPoseData.people[0] && (currentPoseData.people[0].hand_left_keypoints_2d || currentPoseData.people[0].hand_right_keypoints_2d))}, faces=${!!currentPoseData.faces}`);
|
| 1164 |
+
if (currentPoseData.people && currentPoseData.people[0]) {
|
| 1165 |
+
const person = currentPoseData.people[0];
|
| 1166 |
+
debugLog(`🎨 People data: left_hand=${!!person.hand_left_keypoints_2d}, right_hand=${!!person.hand_right_keypoints_2d}, face=${!!person.face_keypoints_2d}`);
|
| 1167 |
+
}
|
| 1168 |
+
|
| 1169 |
+
drawPose(currentPoseData, window.poseEditorGlobals.enableHands, window.poseEditorGlobals.enableFace);
|
| 1170 |
+
debugLog("✅ Real-time redraw completed after keypoint update");
|
| 1171 |
+
} else {
|
| 1172 |
+
debugLog("⚠️ No pose data available for real-time redraw");
|
| 1173 |
+
}
|
| 1174 |
+
} catch (error) {
|
| 1175 |
+
console.error("❌ Error in real-time redraw:", error);
|
| 1176 |
+
}
|
| 1177 |
+
}
|
| 1178 |
+
// 🔧 通常のボディキーポイントドラッグ処理
|
| 1179 |
+
else if (draggedKeypoint >= 0) {
|
| 1180 |
// 🔧 refs互換:オフセット考慮の新座標計算
|
| 1181 |
const newX = mousePos.x - dragOffset.x;
|
| 1182 |
const newY = mousePos.y - dragOffset.y;
|
| 1183 |
|
|
|
|
| 1184 |
// ドラッグ中の座標更新(オフセット適用済み座標で)
|
| 1185 |
updateKeypointPosition(draggedKeypoint, newX, newY);
|
| 1186 |
|
| 1187 |
+
// リアルタイム再描画(ハイライト付き)- エラーハンドリング付き・手顔データ保持強化
|
| 1188 |
+
try {
|
| 1189 |
+
const currentPoseData = window.poseEditorGlobals.poseData || poseData;
|
| 1190 |
+
if (currentPoseData) {
|
| 1191 |
+
// 🫳😊 手と顔データの存在確認
|
| 1192 |
+
debugLog(`🎨 Highlighted redraw: hands=${!!(currentPoseData.people && currentPoseData.people[0] && (currentPoseData.people[0].hand_left_keypoints_2d || currentPoseData.people[0].hand_right_keypoints_2d))}, faces=${!!currentPoseData.faces}`);
|
| 1193 |
+
|
| 1194 |
+
drawPose(currentPoseData, window.poseEditorGlobals.enableHands, window.poseEditorGlobals.enableFace, draggedKeypoint);
|
| 1195 |
+
debugLog("✅ Real-time redraw with highlight completed");
|
| 1196 |
+
} else {
|
| 1197 |
+
debugLog("⚠️ No pose data available for highlighted redraw");
|
| 1198 |
+
}
|
| 1199 |
+
} catch (error) {
|
| 1200 |
+
console.error("❌ Error in highlighted redraw:", error);
|
| 1201 |
+
}
|
| 1202 |
}
|
| 1203 |
|
| 1204 |
} else {
|
|
|
|
| 1213 |
canvas.style.cursor = rectType === window.poseEditorGlobals.rectEditMode ? 'move' : 'default';
|
| 1214 |
}
|
| 1215 |
} else {
|
| 1216 |
+
// 🎯 詳細モードまたは通常モード:キーポイント近くでカーソル変更
|
| 1217 |
+
if (window.poseEditorGlobals.editMode === "詳細モード") {
|
| 1218 |
+
const detailKeypoint = findNearestKeypointInDetailMode(mousePos.x, mousePos.y);
|
| 1219 |
+
canvas.style.cursor = detailKeypoint ? 'grab' : 'default';
|
| 1220 |
+
} else {
|
| 1221 |
+
const keypointIndex = findNearestKeypoint(mousePos.x, mousePos.y);
|
| 1222 |
+
canvas.style.cursor = keypointIndex >= 0 ? 'grab' : 'default';
|
| 1223 |
+
}
|
| 1224 |
}
|
| 1225 |
}
|
| 1226 |
}
|
|
|
|
| 1244 |
} else if (window.poseEditorGlobals.draggedRect) {
|
| 1245 |
debugLog(`Rectangle move drag ended: ${window.poseEditorGlobals.draggedRect}`);
|
| 1246 |
window.poseEditorGlobals.draggedRect = null;
|
| 1247 |
+
} else if (window.poseEditorGlobals.draggedDetailKeypoint) {
|
| 1248 |
+
// 🎯 詳細モードキーポイントドラッグ終了
|
| 1249 |
+
debugLog(`Detail keypoint drag ended: ${window.poseEditorGlobals.draggedDetailKeypoint.type} ${window.poseEditorGlobals.draggedDetailKeypoint.keypointIndex}`);
|
| 1250 |
+
window.poseEditorGlobals.draggedDetailKeypoint = null;
|
| 1251 |
} else if (draggedKeypoint >= 0) {
|
| 1252 |
draggedKeypoint = -1;
|
| 1253 |
}
|
|
|
|
| 1255 |
isDragging = false;
|
| 1256 |
canvas.style.cursor = 'default';
|
| 1257 |
|
| 1258 |
+
// 🚀 編集完了データをGradio送信(データ改変はしない)
|
| 1259 |
sendPoseDataToGradio();
|
| 1260 |
}
|
| 1261 |
}
|
| 1262 |
|
| 1263 |
+
// キーポイント座標更新(refs互換・people形式同期対応)
|
| 1264 |
function updateKeypointPosition(keypointIndex, canvasX, canvasY) {
|
| 1265 |
// グローバルposeDataを参照
|
| 1266 |
const currentPoseData = window.poseEditorGlobals.poseData || poseData;
|
|
|
|
| 1283 |
const clampedDataX = Math.max(0, Math.min(originalRes[0], dataX));
|
| 1284 |
const clampedDataY = Math.max(0, Math.min(originalRes[1], dataY));
|
| 1285 |
|
| 1286 |
+
// 1. candidateリストを更新
|
| 1287 |
const candidates = currentPoseData.bodies.candidate;
|
| 1288 |
if (keypointIndex < candidates.length) {
|
| 1289 |
candidates[keypointIndex][0] = clampedDataX;
|
| 1290 |
candidates[keypointIndex][1] = clampedDataY;
|
| 1291 |
|
|
|
|
| 1292 |
// 🔧 ローカルposeDataも同期更新
|
| 1293 |
if (poseData && poseData.bodies && poseData.bodies.candidate) {
|
| 1294 |
poseData.bodies.candidate[keypointIndex][0] = clampedDataX;
|
| 1295 |
poseData.bodies.candidate[keypointIndex][1] = clampedDataY;
|
| 1296 |
}
|
| 1297 |
}
|
| 1298 |
+
|
| 1299 |
+
// 2. 🚀 people形式にも同期更新(export用)
|
| 1300 |
+
syncBodiesToPeople(currentPoseData);
|
| 1301 |
+
|
| 1302 |
+
// 3. 🫳😊 手と顔データも強制同期(編集後の表示維持のため)
|
| 1303 |
+
if (currentPoseData.people && currentPoseData.people[0]) {
|
| 1304 |
+
const person = currentPoseData.people[0];
|
| 1305 |
+
|
| 1306 |
+
// 💖 手データの同期(people形式のみ)
|
| 1307 |
+
if (person.hand_left_keypoints_2d) {
|
| 1308 |
+
syncHandsToOriginal(currentPoseData, 'left', person.hand_left_keypoints_2d);
|
| 1309 |
+
}
|
| 1310 |
+
if (person.hand_right_keypoints_2d) {
|
| 1311 |
+
syncHandsToOriginal(currentPoseData, 'right', person.hand_right_keypoints_2d);
|
| 1312 |
+
}
|
| 1313 |
+
|
| 1314 |
+
// 顔データの同期
|
| 1315 |
+
if (person.face_keypoints_2d && currentPoseData.faces) {
|
| 1316 |
+
syncFacesToOriginal(currentPoseData, person.face_keypoints_2d);
|
| 1317 |
+
}
|
| 1318 |
+
|
| 1319 |
+
debugLog(`🔄 Hands and face data synced after keypoint update`);
|
| 1320 |
+
}
|
| 1321 |
+
|
| 1322 |
+
debugLog(`🎯 Keypoint ${keypointIndex} updated to (${clampedDataX}, ${clampedDataY})`);
|
| 1323 |
+
}
|
| 1324 |
+
|
| 1325 |
+
// 🚀 bodies.candidateからpeople.pose_keypoints_2dに同期(refs互換)
|
| 1326 |
+
function syncBodiesToPeople(poseData) {
|
| 1327 |
+
if (!poseData || !poseData.bodies || !poseData.bodies.candidate) {
|
| 1328 |
+
return;
|
| 1329 |
+
}
|
| 1330 |
+
|
| 1331 |
+
// people形式が存在しない場合は作成
|
| 1332 |
+
if (!poseData.people) {
|
| 1333 |
+
poseData.people = [{}];
|
| 1334 |
+
}
|
| 1335 |
+
if (!poseData.people[0]) {
|
| 1336 |
+
poseData.people[0] = {};
|
| 1337 |
+
}
|
| 1338 |
+
|
| 1339 |
+
const candidates = poseData.bodies.candidate;
|
| 1340 |
+
const person = poseData.people[0];
|
| 1341 |
+
|
| 1342 |
+
// candidatesからpose_keypoints_2dフラ��ト配列を生成
|
| 1343 |
+
const pose_keypoints_2d = [];
|
| 1344 |
+
for (let i = 0; i < candidates.length; i++) {
|
| 1345 |
+
const candidate = candidates[i];
|
| 1346 |
+
if (candidate && candidate.length >= 2) {
|
| 1347 |
+
pose_keypoints_2d.push(candidate[0]); // x
|
| 1348 |
+
pose_keypoints_2d.push(candidate[1]); // y
|
| 1349 |
+
pose_keypoints_2d.push(candidate[2] || 1.0); // confidence (デフォルト1.0)
|
| 1350 |
+
} else {
|
| 1351 |
+
// 無効なキーポイントは0で埋める
|
| 1352 |
+
pose_keypoints_2d.push(0, 0, 0);
|
| 1353 |
+
}
|
| 1354 |
+
}
|
| 1355 |
+
|
| 1356 |
+
person.pose_keypoints_2d = pose_keypoints_2d;
|
| 1357 |
+
|
| 1358 |
+
// 🫳😊 既存の手と顔データを保持(ボディ編集時に消失しないように)
|
| 1359 |
+
if (!person.hand_left_keypoints_2d && poseData.hands && poseData.hands[0]) {
|
| 1360 |
+
person.hand_left_keypoints_2d = poseData.hands[0];
|
| 1361 |
+
debugLog(`🫳 Left hand data preserved in people format`);
|
| 1362 |
+
}
|
| 1363 |
+
if (!person.hand_right_keypoints_2d && poseData.hands && poseData.hands[1]) {
|
| 1364 |
+
person.hand_right_keypoints_2d = poseData.hands[1];
|
| 1365 |
+
debugLog(`🫳 Right hand data preserved in people format`);
|
| 1366 |
+
}
|
| 1367 |
+
if (!person.face_keypoints_2d && poseData.faces && poseData.faces.length > 0) {
|
| 1368 |
+
person.face_keypoints_2d = poseData.faces[0] || poseData.faces;
|
| 1369 |
+
debugLog(`😊 Face data preserved in people format`);
|
| 1370 |
+
}
|
| 1371 |
+
|
| 1372 |
+
debugLog(`🔄 Bodies synced to people: ${pose_keypoints_2d.length / 3} keypoints, hands preserved: ${!!person.hand_left_keypoints_2d}/${!!person.hand_right_keypoints_2d}, face: ${!!person.face_keypoints_2d}`);
|
| 1373 |
}
|
| 1374 |
|
| 1375 |
+
// Gradioにデータ送信(refs互換・強制changeイベント版)
|
| 1376 |
function sendPoseDataToGradio() {
|
| 1377 |
+
console.log('🔍 [sendPoseDataToGradio] Called');
|
| 1378 |
// グローバルposeDataを参照
|
| 1379 |
const currentPoseData = window.poseEditorGlobals.poseData || poseData;
|
| 1380 |
+
console.log('🔍 [sendPoseDataToGradio] currentPoseData state:', {
|
| 1381 |
+
exists: !!currentPoseData,
|
| 1382 |
+
hasPeople: !!currentPoseData?.people,
|
| 1383 |
+
peopleCount: currentPoseData?.people?.length || 0,
|
| 1384 |
+
hasHandLeft: !!currentPoseData?.people?.[0]?.hand_left_keypoints_2d,
|
| 1385 |
+
hasHandRight: !!currentPoseData?.people?.[0]?.hand_right_keypoints_2d,
|
| 1386 |
+
hasFace: !!currentPoseData?.people?.[0]?.face_keypoints_2d
|
| 1387 |
+
});
|
| 1388 |
|
| 1389 |
if (!currentPoseData) {
|
| 1390 |
debugLog("No poseData available for Gradio send");
|
| 1391 |
return;
|
| 1392 |
}
|
| 1393 |
|
| 1394 |
+
// Issue 038: JavaScript側更新フラグチェック(refs issue043準拠)
|
| 1395 |
+
if (window.poseEditorGlobals && window.poseEditorGlobals.isUpdating) {
|
| 1396 |
+
debugLog("⚠️ JavaScript更新処理中のため、Gradio送信をスキップ");
|
| 1397 |
+
return;
|
| 1398 |
+
}
|
| 1399 |
+
|
| 1400 |
+
// 処理開始フラグを立てる
|
| 1401 |
+
if (window.poseEditorGlobals) {
|
| 1402 |
+
window.poseEditorGlobals.isUpdating = true;
|
| 1403 |
+
debugLog("🔒 JavaScript更新フラグ設定: isUpdating=true");
|
| 1404 |
+
}
|
| 1405 |
+
|
| 1406 |
try {
|
| 1407 |
+
// Issue #038: people形式でPythonに送信(refs互換)
|
| 1408 |
+
const canvasData = {
|
| 1409 |
+
"people": [],
|
| 1410 |
+
"_t": Date.now() // タイムスタンプで強制イベント発火
|
| 1411 |
+
};
|
| 1412 |
|
| 1413 |
+
// 常に最新データで構築(people形式があっても再構築)
|
| 1414 |
+
if (currentPoseData.bodies && currentPoseData.bodies.candidate) {
|
| 1415 |
+
// bodies.candidateからpeople形式に変換
|
| 1416 |
+
const pose_keypoints_2d = [];
|
| 1417 |
+
const candidates = currentPoseData.bodies.candidate;
|
| 1418 |
+
|
| 1419 |
+
for (let i = 0; i < candidates.length; i++) {
|
| 1420 |
+
const candidate = candidates[i];
|
| 1421 |
+
if (candidate && candidate.length >= 2) {
|
| 1422 |
+
pose_keypoints_2d.push(candidate[0]); // x
|
| 1423 |
+
pose_keypoints_2d.push(candidate[1]); // y
|
| 1424 |
+
pose_keypoints_2d.push(candidate.length > 2 ? candidate[2] : 1.0); // confidence
|
| 1425 |
+
}
|
| 1426 |
+
}
|
| 1427 |
+
|
| 1428 |
+
const person = {
|
| 1429 |
+
"pose_keypoints_2d": pose_keypoints_2d
|
| 1430 |
+
};
|
| 1431 |
+
|
| 1432 |
+
// 🫳 手のデータを追加(複数のソースから確実に取得)
|
| 1433 |
+
// people形式から取得を試行
|
| 1434 |
+
let leftHandData = null;
|
| 1435 |
+
let rightHandData = null;
|
| 1436 |
+
let faceData = null;
|
| 1437 |
+
|
| 1438 |
+
// 💖 people形式からのみデータ取得(フォールバック削除)
|
| 1439 |
+
if (currentPoseData.people && currentPoseData.people[0]) {
|
| 1440 |
+
leftHandData = currentPoseData.people[0].hand_left_keypoints_2d || [];
|
| 1441 |
+
rightHandData = currentPoseData.people[0].hand_right_keypoints_2d || [];
|
| 1442 |
+
faceData = currentPoseData.people[0].face_keypoints_2d || [];
|
| 1443 |
+
}
|
| 1444 |
+
if (!faceData && currentPoseData.faces && currentPoseData.faces.length > 0) {
|
| 1445 |
+
faceData = currentPoseData.faces[0] || currentPoseData.faces;
|
| 1446 |
+
}
|
| 1447 |
+
|
| 1448 |
+
// 💖 手と顔データを確実に設定(配列コピーで安全に保持)
|
| 1449 |
+
if (leftHandData && leftHandData.length > 0) {
|
| 1450 |
+
person.hand_left_keypoints_2d = [...leftHandData]; // 配列コピー
|
| 1451 |
+
debugLog(`🫳 Left hand data included (copied): ${leftHandData.length} points`);
|
| 1452 |
+
} else {
|
| 1453 |
+
debugLog(`⚠️ Left hand data missing or empty`);
|
| 1454 |
+
}
|
| 1455 |
+
|
| 1456 |
+
if (rightHandData && rightHandData.length > 0) {
|
| 1457 |
+
person.hand_right_keypoints_2d = [...rightHandData]; // 配列コピー
|
| 1458 |
+
debugLog(`🫳 Right hand data included (copied): ${rightHandData.length} points`);
|
| 1459 |
+
} else {
|
| 1460 |
+
debugLog(`⚠️ Right hand data missing or empty`);
|
| 1461 |
+
}
|
| 1462 |
+
|
| 1463 |
+
if (faceData && faceData.length > 0) {
|
| 1464 |
+
person.face_keypoints_2d = [...faceData]; // 配列コピー
|
| 1465 |
+
debugLog(`😊 Face data included (copied): ${faceData.length} points`);
|
| 1466 |
+
} else {
|
| 1467 |
+
debugLog(`⚠️ Face data missing or empty`);
|
| 1468 |
+
}
|
| 1469 |
+
|
| 1470 |
+
canvasData.people = [person];
|
| 1471 |
+
} else {
|
| 1472 |
+
// bodies.candidateがない場合でも手と顔データがあれば送信
|
| 1473 |
+
debugLog("⚠️ No bodies data available, trying to send hands/face only");
|
| 1474 |
+
|
| 1475 |
+
const person = {
|
| 1476 |
+
"pose_keypoints_2d": [] // 空のボディデータ
|
| 1477 |
+
};
|
| 1478 |
+
|
| 1479 |
+
// 手と顔データのみ追加
|
| 1480 |
+
if (currentPoseData.people && currentPoseData.people[0]) {
|
| 1481 |
+
const existingPerson = currentPoseData.people[0];
|
| 1482 |
+
if (existingPerson.hand_left_keypoints_2d) {
|
| 1483 |
+
person.hand_left_keypoints_2d = existingPerson.hand_left_keypoints_2d;
|
| 1484 |
+
}
|
| 1485 |
+
if (existingPerson.hand_right_keypoints_2d) {
|
| 1486 |
+
person.hand_right_keypoints_2d = existingPerson.hand_right_keypoints_2d;
|
| 1487 |
+
}
|
| 1488 |
+
if (existingPerson.face_keypoints_2d) {
|
| 1489 |
+
person.face_keypoints_2d = existingPerson.face_keypoints_2d;
|
| 1490 |
+
}
|
| 1491 |
+
}
|
| 1492 |
+
|
| 1493 |
+
// 💖 people形式で手・顔データ存在確認
|
| 1494 |
+
const hasHandsOrFace = currentPoseData.people && currentPoseData.people[0] &&
|
| 1495 |
+
(currentPoseData.people[0].hand_left_keypoints_2d ||
|
| 1496 |
+
currentPoseData.people[0].hand_right_keypoints_2d ||
|
| 1497 |
+
currentPoseData.people[0].face_keypoints_2d) ||
|
| 1498 |
+
currentPoseData.faces;
|
| 1499 |
+
if (hasHandsOrFace) {
|
| 1500 |
+
canvasData.people = [person];
|
| 1501 |
+
debugLog("🔄 Sending hands/face only data");
|
| 1502 |
+
}
|
| 1503 |
+
}
|
| 1504 |
+
|
| 1505 |
+
// 解像度情報を保持
|
| 1506 |
+
if (currentPoseData.resolution) {
|
| 1507 |
+
canvasData.resolution = currentPoseData.resolution;
|
| 1508 |
+
}
|
| 1509 |
+
|
| 1510 |
+
// 送信前の最終データ確認
|
| 1511 |
+
console.log('🔍 [sendPoseDataToGradio] Final canvasData being sent:', {
|
| 1512 |
+
hasPeople: !!canvasData.people,
|
| 1513 |
+
peopleCount: canvasData.people?.length || 0,
|
| 1514 |
+
hasHandLeft: !!canvasData.people?.[0]?.hand_left_keypoints_2d,
|
| 1515 |
+
hasHandRight: !!canvasData.people?.[0]?.hand_right_keypoints_2d,
|
| 1516 |
+
hasFace: !!canvasData.people?.[0]?.face_keypoints_2d,
|
| 1517 |
+
handLeftLength: canvasData.people?.[0]?.hand_left_keypoints_2d?.length || 0,
|
| 1518 |
+
handRightLength: canvasData.people?.[0]?.hand_right_keypoints_2d?.length || 0,
|
| 1519 |
+
faceLength: canvasData.people?.[0]?.face_keypoints_2d?.length || 0
|
| 1520 |
+
});
|
| 1521 |
+
|
| 1522 |
+
const jsonString = JSON.stringify(canvasData);
|
| 1523 |
+
console.log('🎯 [sendPoseDataToGradio] People形式でGradioに送信:', canvasData.people.length, 'people');
|
| 1524 |
+
|
| 1525 |
+
// 専用の隠しテキストボックスを探して更新
|
| 1526 |
+
const jsUpdateTextbox = document.querySelector('#js_pose_update textarea');
|
| 1527 |
|
| 1528 |
+
if (jsUpdateTextbox) {
|
| 1529 |
+
jsUpdateTextbox.value = jsonString;
|
| 1530 |
+
jsUpdateTextbox.dispatchEvent(new Event('input', { bubbles: true }));
|
| 1531 |
+
debugLog('✅ Pose data sent to Gradio via js_pose_update with timestamp');
|
| 1532 |
+
return;
|
| 1533 |
+
}
|
| 1534 |
+
|
| 1535 |
+
// フォールバック:従来の方法
|
| 1536 |
+
const textareas = document.querySelectorAll('textarea');
|
| 1537 |
for (const textarea of textareas) {
|
| 1538 |
const currentValue = textarea.value || '';
|
| 1539 |
if (currentValue.includes('bodies') || currentValue.includes('candidate') || currentValue.trim() === '') {
|
| 1540 |
textarea.value = jsonString;
|
| 1541 |
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
| 1542 |
+
debugLog('⚠️ Pose data sent to Gradio via fallback method with timestamp');
|
| 1543 |
return;
|
| 1544 |
}
|
| 1545 |
}
|
|
|
|
| 1547 |
|
| 1548 |
} catch (error) {
|
| 1549 |
debugLog(`Error sending data to Gradio: ${error.message}`);
|
| 1550 |
+
} finally {
|
| 1551 |
+
// Issue 038: 処理完了後は必ずフラグを解除(refs issue043準拠)
|
| 1552 |
+
if (window.poseEditorGlobals) {
|
| 1553 |
+
const oldFlag = window.poseEditorGlobals.isUpdating;
|
| 1554 |
+
window.poseEditorGlobals.isUpdating = false;
|
| 1555 |
+
debugLog(`🔓 JavaScript更新処理完了 - フラグ解除: ${oldFlag} → ${window.poseEditorGlobals.isUpdating}`);
|
| 1556 |
+
}
|
| 1557 |
}
|
| 1558 |
}
|
| 1559 |
|
|
|
|
| 1576 |
// キャンバスクリア
|
| 1577 |
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
| 1578 |
|
| 1579 |
+
// 🎨 背景画像描画
|
| 1580 |
+
drawBackground();
|
| 1581 |
+
|
| 1582 |
// 🔧 矩形編集モード中でない場合のみ矩形を再計算
|
| 1583 |
if (window.poseEditorGlobals.editMode === "簡易モード" && !window.poseEditorGlobals.rectEditModeActive) {
|
| 1584 |
window.poseEditorGlobals.currentRects = { leftHand: null, rightHand: null, face: null };
|
| 1585 |
}
|
| 1586 |
+
// 🎯 詳細モードでは常に矩形をクリア
|
| 1587 |
+
else if (window.poseEditorGlobals.editMode === "詳細モード") {
|
| 1588 |
+
window.poseEditorGlobals.currentRects = { leftHand: null, rightHand: null, face: null };
|
| 1589 |
+
}
|
| 1590 |
|
| 1591 |
// 📐 解像度情報の取得(手と顔描画のため)
|
| 1592 |
const originalRes = currentPoseData.resolution || [512, 512];
|
|
|
|
| 1597 |
// ボディの描画(ハイライト対応)
|
| 1598 |
drawBody(currentPoseData, highlightIndex);
|
| 1599 |
|
| 1600 |
+
// 手の描画(設定制御・座標変換パラメータ付き・エラー耐性強化)
|
| 1601 |
+
if (enableHands) {
|
| 1602 |
+
try {
|
| 1603 |
+
// 🚀 refs互換:編集済みのhand_keypoints_2dを優先使用、フォールバック付き
|
| 1604 |
+
let handsDataForDrawing = null;
|
| 1605 |
+
|
| 1606 |
+
if (currentPoseData.people && currentPoseData.people[0]) {
|
| 1607 |
+
const person = currentPoseData.people[0];
|
| 1608 |
+
handsDataForDrawing = [
|
| 1609 |
+
person.hand_left_keypoints_2d || [],
|
| 1610 |
+
person.hand_right_keypoints_2d || []
|
| 1611 |
+
];
|
| 1612 |
+
// 💖 people形式のみサポート、古いhands形式は削除
|
| 1613 |
+
} else {
|
| 1614 |
+
handsDataForDrawing = [[], []]; // 空の手データ
|
| 1615 |
+
}
|
| 1616 |
+
|
| 1617 |
+
if (handsDataForDrawing && handsDataForDrawing.length >= 2) {
|
| 1618 |
+
drawHands(handsDataForDrawing, originalRes, scaleX, scaleY);
|
| 1619 |
+
debugLog(`🫳 Hands drawn: left=${handsDataForDrawing[0].length}, right=${handsDataForDrawing[1].length}`);
|
| 1620 |
+
}
|
| 1621 |
+
} catch (error) {
|
| 1622 |
+
console.error("❌ Error drawing hands:", error);
|
| 1623 |
+
}
|
| 1624 |
|
| 1625 |
// 🔧 簡易モード:手の矩形描画(編集モード中は再計算しない)
|
| 1626 |
if (window.poseEditorGlobals.editMode === "簡易モード") {
|
|
|
|
| 1629 |
if (currentPoseData.people && currentPoseData.people[0]) {
|
| 1630 |
const person = currentPoseData.people[0];
|
| 1631 |
const editedHandsData = [
|
| 1632 |
+
person.hand_left_keypoints_2d || [],
|
| 1633 |
+
person.hand_right_keypoints_2d || []
|
| 1634 |
];
|
| 1635 |
drawHandRectangles(editedHandsData, originalRes, scaleX, scaleY);
|
| 1636 |
} else {
|
| 1637 |
+
// 💖 people形式のみサポート、空データで矩形なし
|
| 1638 |
+
drawHandRectangles([[], []], originalRes, scaleX, scaleY);
|
| 1639 |
}
|
| 1640 |
} else {
|
| 1641 |
// 編集モード中:既存の矩形を描画(再計算��ない)
|
|
|
|
| 1648 |
debugLog("No hand data available");
|
| 1649 |
}
|
| 1650 |
|
| 1651 |
+
// 顔の描画(設定制御・座標変換パラメータ付き・エラー耐性強化)
|
| 1652 |
+
if (enableFace) {
|
| 1653 |
+
try {
|
| 1654 |
+
// 🚀 refs互換:編集済みのface_keypoints_2dを優先使用、フォールバック付き
|
| 1655 |
+
let facesDataForDrawing = null;
|
| 1656 |
+
|
| 1657 |
+
if (currentPoseData.people && currentPoseData.people[0] && currentPoseData.people[0].face_keypoints_2d) {
|
| 1658 |
+
facesDataForDrawing = [currentPoseData.people[0].face_keypoints_2d];
|
| 1659 |
+
} else if (currentPoseData.faces && Array.isArray(currentPoseData.faces)) {
|
| 1660 |
+
facesDataForDrawing = currentPoseData.faces;
|
| 1661 |
+
} else {
|
| 1662 |
+
facesDataForDrawing = [[]]; // 空の顔データ
|
| 1663 |
+
}
|
| 1664 |
+
|
| 1665 |
+
if (facesDataForDrawing && facesDataForDrawing.length > 0) {
|
| 1666 |
+
drawFaces(facesDataForDrawing, originalRes, scaleX, scaleY);
|
| 1667 |
+
debugLog(`😊 Face drawn: ${facesDataForDrawing[0].length} keypoints`);
|
| 1668 |
+
}
|
| 1669 |
+
} catch (error) {
|
| 1670 |
+
console.error("❌ Error drawing face:", error);
|
| 1671 |
+
}
|
| 1672 |
|
| 1673 |
// 🔧 簡易モード:顔の矩形描画(編集モード中は再計算しない)
|
| 1674 |
if (window.poseEditorGlobals.editMode === "簡易モード") {
|
|
|
|
| 1692 |
}
|
| 1693 |
}
|
| 1694 |
|
| 1695 |
+
// 🎨 背景画像描画(refs互換)
|
| 1696 |
+
function drawBackground() {
|
| 1697 |
+
if (!isCanvasReady()) return;
|
| 1698 |
+
|
| 1699 |
+
const canvas = window.poseEditorGlobals.canvas;
|
| 1700 |
+
const ctx = window.poseEditorGlobals.ctx;
|
| 1701 |
+
|
| 1702 |
+
// デフォルト背景(白)
|
| 1703 |
+
ctx.fillStyle = '#ffffff';
|
| 1704 |
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
| 1705 |
+
|
| 1706 |
+
// 背景画像がある場合は暗くして描画
|
| 1707 |
+
if (window.poseEditorGlobals.backgroundImage) {
|
| 1708 |
+
const img = window.poseEditorGlobals.backgroundImage;
|
| 1709 |
+
|
| 1710 |
+
// アスペクト比を保持してCanvas内に収める
|
| 1711 |
+
const imgAspect = img.width / img.height;
|
| 1712 |
+
const canvasAspect = canvas.width / canvas.height;
|
| 1713 |
+
|
| 1714 |
+
let drawWidth, drawHeight, offsetX, offsetY;
|
| 1715 |
+
|
| 1716 |
+
if (imgAspect > canvasAspect) {
|
| 1717 |
+
// 画像の方が横長 → 幅をCanvasに合わせる
|
| 1718 |
+
drawWidth = canvas.width;
|
| 1719 |
+
drawHeight = canvas.width / imgAspect;
|
| 1720 |
+
offsetX = 0;
|
| 1721 |
+
offsetY = (canvas.height - drawHeight) / 2;
|
| 1722 |
+
} else {
|
| 1723 |
+
// 画像の方が縦長 → 高さをCanvasに合わせる
|
| 1724 |
+
drawHeight = canvas.height;
|
| 1725 |
+
drawWidth = canvas.height * imgAspect;
|
| 1726 |
+
offsetX = (canvas.width - drawWidth) / 2;
|
| 1727 |
+
offsetY = 0;
|
| 1728 |
+
}
|
| 1729 |
+
|
| 1730 |
+
// 画像を暗くして描画(30%透明度)
|
| 1731 |
+
ctx.globalAlpha = 0.3;
|
| 1732 |
+
ctx.drawImage(img, offsetX, offsetY, drawWidth, drawHeight);
|
| 1733 |
+
ctx.globalAlpha = 1.0; // 透明度を元に戻す
|
| 1734 |
+
|
| 1735 |
+
debugLog(`🎨 Background image drawn: ${drawWidth}x${drawHeight} at (${offsetX}, ${offsetY})`);
|
| 1736 |
+
}
|
| 1737 |
+
}
|
| 1738 |
+
|
| 1739 |
// ボディ描画(ハイライト対応)
|
| 1740 |
function drawBody(poseData, highlightIndex = -1) {
|
| 1741 |
if (!poseData.bodies || !poseData.bodies.candidate) {
|
|
|
|
| 2084 |
poseData = pose_json_str;
|
| 2085 |
}
|
| 2086 |
|
| 2087 |
+
// 💖 グローバルposeDataを更新(但し、people形式チェック付き)
|
| 2088 |
+
console.log('🔍 [gradioCanvasUpdate] Received poseData structure:', {
|
| 2089 |
+
hasPeople: !!poseData?.people,
|
| 2090 |
+
peopleCount: poseData?.people?.length || 0,
|
| 2091 |
+
hasHandLeft: !!poseData?.people?.[0]?.hand_left_keypoints_2d,
|
| 2092 |
+
hasHandRight: !!poseData?.people?.[0]?.hand_right_keypoints_2d,
|
| 2093 |
+
hasFace: !!poseData?.people?.[0]?.face_keypoints_2d,
|
| 2094 |
+
hasBodies: !!poseData?.bodies,
|
| 2095 |
+
hasHands: !!poseData?.hands,
|
| 2096 |
+
hasFaces: !!poseData?.faces
|
| 2097 |
+
});
|
| 2098 |
+
|
| 2099 |
+
// 💥 既存の手・顔データを保護!Python側データで上書きしない
|
| 2100 |
+
const existingPoseData = window.poseEditorGlobals.poseData;
|
| 2101 |
+
if (existingPoseData && existingPoseData.people && existingPoseData.people[0] &&
|
| 2102 |
+
poseData && (!poseData.people || !poseData.people[0])) {
|
| 2103 |
+
|
| 2104 |
+
console.log('🛡️ [gradioCanvasUpdate] Protecting existing people data from overwrite');
|
| 2105 |
+
console.log('🔍 [gradioCanvasUpdate] Existing data:', {
|
| 2106 |
+
hasHandLeft: !!existingPoseData.people[0].hand_left_keypoints_2d,
|
| 2107 |
+
hasHandRight: !!existingPoseData.people[0].hand_right_keypoints_2d,
|
| 2108 |
+
hasFace: !!existingPoseData.people[0].face_keypoints_2d
|
| 2109 |
+
});
|
| 2110 |
+
|
| 2111 |
+
// Python側データを古い形式に変換してからマージ
|
| 2112 |
+
if (poseData.bodies || poseData.hands || poseData.faces) {
|
| 2113 |
+
// 既存のpeople形式を保持し、bodies部分だけ更新
|
| 2114 |
+
const preservedPoseData = JSON.parse(JSON.stringify(existingPoseData));
|
| 2115 |
+
|
| 2116 |
+
// bodies.candidateがある場合は pose_keypoints_2d を更新
|
| 2117 |
+
if (poseData.bodies && poseData.bodies.candidate) {
|
| 2118 |
+
const pose_keypoints_2d = [];
|
| 2119 |
+
for (const candidate of poseData.bodies.candidate) {
|
| 2120 |
+
if (candidate && candidate.length >= 2) {
|
| 2121 |
+
pose_keypoints_2d.push(candidate[0], candidate[1], candidate[2] || 1.0);
|
| 2122 |
+
}
|
| 2123 |
+
}
|
| 2124 |
+
preservedPoseData.people[0].pose_keypoints_2d = pose_keypoints_2d;
|
| 2125 |
+
}
|
| 2126 |
+
|
| 2127 |
+
// 手と顔データは既存を保持(Python側で消失している可能性があるため)
|
| 2128 |
+
console.log('🔍 [gradioCanvasUpdate] Preserving hands/face data from existing poseData');
|
| 2129 |
+
|
| 2130 |
+
window.poseEditorGlobals.poseData = preservedPoseData;
|
| 2131 |
+
poseData = preservedPoseData; // 描画用も更新
|
| 2132 |
+
} else {
|
| 2133 |
+
// people形式で来た場合はそのまま使用
|
| 2134 |
+
window.poseEditorGlobals.poseData = poseData;
|
| 2135 |
+
}
|
| 2136 |
+
} else {
|
| 2137 |
+
// 通常通り更新
|
| 2138 |
+
window.poseEditorGlobals.poseData = poseData;
|
| 2139 |
+
}
|
| 2140 |
|
| 2141 |
+
// 💖 originalKeypointsも設定(但し、baseOriginalKeypointsは保護)
|
| 2142 |
+
if (poseData && poseData.people && poseData.people[0]) {
|
| 2143 |
+
// 💥 baseOriginalKeypointsは上書きしない!(編集セッション保持のため)
|
| 2144 |
+
if (!window.poseEditorGlobals.baseOriginalKeypoints) {
|
| 2145 |
+
window.poseEditorGlobals.baseOriginalKeypoints = JSON.parse(JSON.stringify(poseData));
|
| 2146 |
+
console.log("💖 [gradioCanvasUpdate] baseOriginalKeypoints set for FIRST time");
|
| 2147 |
+
} else {
|
| 2148 |
+
console.log("💖 [gradioCanvasUpdate] baseOriginalKeypoints PROTECTED from overwrite");
|
| 2149 |
+
}
|
| 2150 |
+
|
| 2151 |
+
// originalKeypointsは更新してOK(作業用)
|
| 2152 |
+
window.poseEditorGlobals.originalKeypoints = JSON.parse(JSON.stringify(poseData));
|
| 2153 |
+
console.log("🔧 [gradioCanvasUpdate] originalKeypoints set from gradioCanvasUpdate for template/upload");
|
| 2154 |
+
|
| 2155 |
+
// デバッグ:手のデータ確認
|
| 2156 |
+
const person = poseData.people[0];
|
| 2157 |
+
if (person.hand_left_keypoints_2d) {
|
| 2158 |
+
debugLog(`🫳 Left hand keypoints: ${person.hand_left_keypoints_2d.length} elements`);
|
| 2159 |
+
} else {
|
| 2160 |
+
debugLog("🫳 No left hand keypoints found");
|
| 2161 |
+
}
|
| 2162 |
+
if (person.hand_right_keypoints_2d) {
|
| 2163 |
+
debugLog(`🫳 Right hand keypoints: ${person.hand_right_keypoints_2d.length} elements`);
|
| 2164 |
+
} else {
|
| 2165 |
+
debugLog("🫳 No right hand keypoints found");
|
| 2166 |
+
}
|
| 2167 |
+
}
|
| 2168 |
|
| 2169 |
if (!isCanvasReady()) {
|
| 2170 |
debugLog("Canvas not ready, initializing...");
|
|
|
|
| 2229 |
window.poseEditorGlobals.enableFace = enableFace;
|
| 2230 |
window.poseEditorGlobals.editMode = editMode;
|
| 2231 |
|
| 2232 |
+
// 🎯 詳細モードに切り替えた時は矩形編集モードを確実に終了
|
| 2233 |
+
if (editMode === "詳細モード") {
|
| 2234 |
+
window.poseEditorGlobals.rectEditMode = null;
|
| 2235 |
+
window.poseEditorGlobals.rectEditModeActive = false;
|
| 2236 |
+
window.poseEditorGlobals.draggedRectControl = null;
|
| 2237 |
+
window.poseEditorGlobals.draggedRect = null;
|
| 2238 |
+
window.poseEditorGlobals.currentRects = { leftHand: null, rightHand: null, face: null };
|
| 2239 |
+
|
| 2240 |
+
// 🔧 編集関連の状態も完全にクリア(連続編集対応)
|
| 2241 |
+
window.poseEditorGlobals.baseOriginalKeypoints = null;
|
| 2242 |
+
window.poseEditorGlobals.originalKeypoints = null;
|
| 2243 |
+
window.poseEditorGlobals.originalRect = null;
|
| 2244 |
+
window.poseEditorGlobals.rectEditInfo = null;
|
| 2245 |
+
window.poseEditorGlobals.dragStartPos = { x: 0, y: 0 };
|
| 2246 |
+
|
| 2247 |
+
debugLog("🔧 All rectangle edit states cleared when switching to detailed mode");
|
| 2248 |
+
}
|
| 2249 |
+
|
| 2250 |
+
// 🔧 設定更新時は強制的に再描画
|
| 2251 |
+
debugLog(`🎨 Display settings updated: hands=${enableHands}, face=${enableFace}, mode=${editMode}`);
|
| 2252 |
+
|
| 2253 |
const currentPoseData = window.poseEditorGlobals.poseData || poseData;
|
| 2254 |
if (currentPoseData && Object.keys(currentPoseData).length > 0) {
|
| 2255 |
+
debugLog("🔄 Redrawing pose with new settings...");
|
| 2256 |
drawPose(currentPoseData, enableHands, enableFace);
|
| 2257 |
+
} else {
|
| 2258 |
+
debugLog("⚠️ No pose data available for redraw");
|
| 2259 |
+
// データがない場合でも背景だけ描画し直す
|
| 2260 |
+
if (isCanvasReady()) {
|
| 2261 |
+
const canvas = window.poseEditorGlobals.canvas;
|
| 2262 |
+
const ctx = window.poseEditorGlobals.ctx;
|
| 2263 |
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
| 2264 |
+
drawBackground();
|
| 2265 |
+
}
|
| 2266 |
}
|
| 2267 |
|
| 2268 |
};
|
|
|
|
| 2279 |
}
|
| 2280 |
};
|
| 2281 |
|
| 2282 |
+
// 🎨 背景画像設定機能(refs互換)
|
| 2283 |
+
window.setBackgroundImage = function(imageData) {
|
| 2284 |
+
if (!imageData) {
|
| 2285 |
+
// 背景画像をクリア
|
| 2286 |
+
window.poseEditorGlobals.backgroundImage = null;
|
| 2287 |
+
debugLog("🎨 Background image cleared");
|
| 2288 |
+
|
| 2289 |
+
// 現在のポーズデータがあれば再描画
|
| 2290 |
+
const currentPoseData = window.poseEditorGlobals.poseData;
|
| 2291 |
+
if (currentPoseData && Object.keys(currentPoseData).length > 0) {
|
| 2292 |
+
drawPose(currentPoseData, window.poseEditorGlobals.enableHands, window.poseEditorGlobals.enableFace);
|
| 2293 |
+
}
|
| 2294 |
+
return;
|
| 2295 |
+
}
|
| 2296 |
+
|
| 2297 |
+
const img = new Image();
|
| 2298 |
+
img.onload = function() {
|
| 2299 |
+
window.poseEditorGlobals.backgroundImage = img;
|
| 2300 |
+
debugLog(`🎨 Background image loaded: ${img.width}x${img.height}`);
|
| 2301 |
+
|
| 2302 |
+
// 背景画像が設定されたらCanvas再描画
|
| 2303 |
+
const currentPoseData = window.poseEditorGlobals.poseData;
|
| 2304 |
+
if (currentPoseData && Object.keys(currentPoseData).length > 0) {
|
| 2305 |
+
drawPose(currentPoseData, window.poseEditorGlobals.enableHands, window.poseEditorGlobals.enableFace);
|
| 2306 |
+
}
|
| 2307 |
+
};
|
| 2308 |
+
|
| 2309 |
+
img.onerror = function() {
|
| 2310 |
+
debugLog("🚨 Failed to load background image");
|
| 2311 |
+
};
|
| 2312 |
+
|
| 2313 |
+
// Base64データまたはURLから画像を設定
|
| 2314 |
+
if (typeof imageData === 'string') {
|
| 2315 |
+
img.src = imageData;
|
| 2316 |
+
} else if (imageData.url) {
|
| 2317 |
+
img.src = imageData.url;
|
| 2318 |
+
} else if (imageData.path) {
|
| 2319 |
+
img.src = imageData.path;
|
| 2320 |
+
}
|
| 2321 |
+
};
|
| 2322 |
+
|
| 2323 |
// Canvas操作時の通知
|
| 2324 |
function notifyCanvasOperation(message) {
|
| 2325 |
showToast('info', message);
|
|
|
|
| 2382 |
function drawHandRectangles(handsData, originalRes, scaleX, scaleY) {
|
| 2383 |
if (!handsData || handsData.length === 0) return;
|
| 2384 |
|
| 2385 |
+
debugLog(`🎨 Drawing hand rectangles in mode: ${window.poseEditorGlobals.editMode}`);
|
| 2386 |
+
|
| 2387 |
const ctx = window.poseEditorGlobals.ctx;
|
| 2388 |
|
| 2389 |
// 矩形の色定義(refs互換)
|
|
|
|
| 2396 |
if (rect) {
|
| 2397 |
const handType = HAND_TYPES[handIndex] || `hand_${handIndex}`;
|
| 2398 |
|
| 2399 |
+
// 🔧 グローバルに矩形情報保存(編集モード中は既存の矩形を保持)
|
| 2400 |
+
if (!window.poseEditorGlobals.currentRects[handType] || !window.poseEditorGlobals.rectEditModeActive) {
|
| 2401 |
+
window.poseEditorGlobals.currentRects[handType] = rect;
|
| 2402 |
+
debugLog(`🔧 Hand rect calculated for ${handType}: ${JSON.stringify(rect)}`);
|
| 2403 |
+
} else {
|
| 2404 |
+
debugLog(`🔧 Hand rect preserved for ${handType} (edit mode active)`);
|
| 2405 |
+
}
|
| 2406 |
|
| 2407 |
+
// 描画は現在の矩形を使用(編集中は保存されている矩形)
|
| 2408 |
+
const drawRect = window.poseEditorGlobals.currentRects[handType] || rect;
|
| 2409 |
+
drawEditableRect(ctx, drawRect, HAND_RECT_COLORS[handIndex % 2], handType);
|
| 2410 |
}
|
| 2411 |
}
|
| 2412 |
});
|
|
|
|
| 2416 |
function drawFaceRectangles(facesData, originalRes, scaleX, scaleY) {
|
| 2417 |
if (!facesData || facesData.length === 0) return;
|
| 2418 |
|
| 2419 |
+
debugLog(`🎨 Drawing face rectangles in mode: ${window.poseEditorGlobals.editMode}`);
|
| 2420 |
+
|
| 2421 |
const ctx = window.poseEditorGlobals.ctx;
|
| 2422 |
|
| 2423 |
// 矩形の色定義(refs互換)
|
|
|
|
| 2427 |
if (face && face.length > 0) {
|
| 2428 |
const rect = calculateFaceRect(face, originalRes, scaleX, scaleY);
|
| 2429 |
if (rect) {
|
| 2430 |
+
// 🔧 グローバルに矩形情報保存(編集モード中は既存の矩形を保持)
|
| 2431 |
+
if (!window.poseEditorGlobals.currentRects.face || !window.poseEditorGlobals.rectEditModeActive) {
|
| 2432 |
+
window.poseEditorGlobals.currentRects.face = rect;
|
| 2433 |
+
debugLog(`🔧 Face rect calculated: ${JSON.stringify(rect)}`);
|
| 2434 |
+
} else {
|
| 2435 |
+
debugLog(`🔧 Face rect preserved (edit mode active)`);
|
| 2436 |
+
}
|
| 2437 |
|
| 2438 |
+
// 描画は現在の矩形を使用(編集中は保存されている矩形)
|
| 2439 |
+
const drawRect = window.poseEditorGlobals.currentRects.face || rect;
|
| 2440 |
+
drawEditableRect(ctx, drawRect, FACE_RECT_COLOR, 'face');
|
| 2441 |
}
|
| 2442 |
}
|
| 2443 |
}
|
|
|
|
| 2588 |
drawBody(currentPoseData, -1);
|
| 2589 |
|
| 2590 |
// 手の描画(設定制御・座標変換パラメータ付き)
|
| 2591 |
+
// 💖 people形式で手を描画
|
| 2592 |
+
if (window.poseEditorGlobals.enableHands && currentPoseData.people && currentPoseData.people[0]) {
|
| 2593 |
+
const person = currentPoseData.people[0];
|
| 2594 |
+
const handsData = [
|
| 2595 |
+
person.hand_left_keypoints_2d || [],
|
| 2596 |
+
person.hand_right_keypoints_2d || []
|
| 2597 |
+
];
|
| 2598 |
+
drawHands(handsData, originalRes, scaleX, scaleY);
|
| 2599 |
}
|
| 2600 |
|
| 2601 |
// 顔の描画(設定制御・座標変換パラメータ付き)
|
|
|
|
| 2792 |
window.poseEditorGlobals.currentRects[rectType] = newRect;
|
| 2793 |
|
| 2794 |
|
| 2795 |
+
// 🔧 手・顔キーポイントの座標も更新(直接矩形変換版)
|
| 2796 |
+
if (controlPoint) {
|
| 2797 |
+
transformKeypointsDirectly(rectType, originalRect, newRect);
|
| 2798 |
+
}
|
| 2799 |
|
| 2800 |
// 🚀 refs互換:編集中もリアルタイム描画
|
| 2801 |
const currentPoseData = window.poseEditorGlobals.poseData || poseData;
|
|
|
|
| 2812 |
function initializeRectEditInfo() {
|
| 2813 |
debugLog('🚀 Initializing rectEditInfo for all rect types');
|
| 2814 |
|
| 2815 |
+
// 🔧 前回の状態をクリアしてから初期化(連続編集対応)
|
| 2816 |
+
window.poseEditorGlobals.rectEditInfo = {};
|
| 2817 |
+
window.poseEditorGlobals.baseOriginalKeypoints = null;
|
| 2818 |
+
window.poseEditorGlobals.originalKeypoints = null;
|
| 2819 |
+
window.poseEditorGlobals.originalRect = null;
|
| 2820 |
|
| 2821 |
const rectTypes = ['face', 'leftHand', 'rightHand'];
|
| 2822 |
|
|
|
|
| 2831 |
}
|
| 2832 |
}
|
| 2833 |
|
| 2834 |
+
// 💖 元のキーポイントを保存(連続編集対応:ベースデータは初回のみ保存)
|
| 2835 |
const currentPoseData = window.poseEditorGlobals.poseData || poseData;
|
| 2836 |
if (currentPoseData) {
|
| 2837 |
+
// 💥 ベースは初回のみ保存!編集済みデータで上書きしない
|
| 2838 |
+
if (!window.poseEditorGlobals.baseOriginalKeypoints) {
|
| 2839 |
+
window.poseEditorGlobals.baseOriginalKeypoints = JSON.parse(JSON.stringify(currentPoseData));
|
| 2840 |
+
console.log('💖 [initializeRectEditInfo] Base original keypoints saved for FIRST editing session');
|
| 2841 |
+
|
| 2842 |
+
// ベースデータの詳細確認
|
| 2843 |
+
console.log('🔍 [initializeRectEditInfo] Base data details:', {
|
| 2844 |
+
hasHandLeft: !!window.poseEditorGlobals.baseOriginalKeypoints.people?.[0]?.hand_left_keypoints_2d,
|
| 2845 |
+
hasHandRight: !!window.poseEditorGlobals.baseOriginalKeypoints.people?.[0]?.hand_right_keypoints_2d,
|
| 2846 |
+
hasFace: !!window.poseEditorGlobals.baseOriginalKeypoints.people?.[0]?.face_keypoints_2d,
|
| 2847 |
+
handLeftLength: window.poseEditorGlobals.baseOriginalKeypoints.people?.[0]?.hand_left_keypoints_2d?.length || 0,
|
| 2848 |
+
handRightLength: window.poseEditorGlobals.baseOriginalKeypoints.people?.[0]?.hand_right_keypoints_2d?.length || 0,
|
| 2849 |
+
faceLength: window.poseEditorGlobals.baseOriginalKeypoints.people?.[0]?.face_keypoints_2d?.length || 0
|
| 2850 |
+
});
|
| 2851 |
+
} else {
|
| 2852 |
+
console.log('💖 [initializeRectEditInfo] Base original keypoints already exists - keeping original data');
|
| 2853 |
+
}
|
| 2854 |
+
|
| 2855 |
+
// 作業用は常にベースからコピー(編集済みデータではなく!)
|
| 2856 |
+
window.poseEditorGlobals.originalKeypoints = JSON.parse(JSON.stringify(window.poseEditorGlobals.baseOriginalKeypoints));
|
| 2857 |
+
console.log('💖 [initializeRectEditInfo] Working original keypoints restored from base for current editing session');
|
| 2858 |
+
|
| 2859 |
+
// 作業用データの詳細確認
|
| 2860 |
+
console.log('🔍 [initializeRectEditInfo] Working data details:', {
|
| 2861 |
+
hasHandLeft: !!window.poseEditorGlobals.originalKeypoints.people?.[0]?.hand_left_keypoints_2d,
|
| 2862 |
+
hasHandRight: !!window.poseEditorGlobals.originalKeypoints.people?.[0]?.hand_right_keypoints_2d,
|
| 2863 |
+
hasFace: !!window.poseEditorGlobals.originalKeypoints.people?.[0]?.face_keypoints_2d,
|
| 2864 |
+
handLeftLength: window.poseEditorGlobals.originalKeypoints.people?.[0]?.hand_left_keypoints_2d?.length || 0,
|
| 2865 |
+
handRightLength: window.poseEditorGlobals.originalKeypoints.people?.[0]?.hand_right_keypoints_2d?.length || 0,
|
| 2866 |
+
faceLength: window.poseEditorGlobals.originalKeypoints.people?.[0]?.face_keypoints_2d?.length || 0
|
| 2867 |
+
});
|
| 2868 |
}
|
| 2869 |
}
|
| 2870 |
|
|
|
|
| 3033 |
|
| 3034 |
}
|
| 3035 |
|
| 3036 |
+
// 🔧 矩形移動ドラッグ処理(refs互換修正版)
|
| 3037 |
function updateRectMoveDrag(mouseX, mouseY) {
|
| 3038 |
const rectType = window.poseEditorGlobals.draggedRect;
|
| 3039 |
if (!rectType) return;
|
|
|
|
| 3041 |
const rect = window.poseEditorGlobals.currentRects[rectType];
|
| 3042 |
if (!rect) return;
|
| 3043 |
|
| 3044 |
+
const startPos = window.poseEditorGlobals.dragStartPos;
|
| 3045 |
+
const deltaX = mouseX - startPos.x;
|
| 3046 |
+
const deltaY = mouseY - startPos.y;
|
| 3047 |
|
| 3048 |
+
debugLog(`🔧 矩形移動ドラッグ: ${rectType}, delta=(${deltaX}, ${deltaY})`);
|
|
|
|
|
|
|
| 3049 |
|
| 3050 |
+
// 矩形の位置を更新(refs方式)
|
| 3051 |
+
const newRect = {
|
| 3052 |
+
...rect,
|
| 3053 |
+
x: rect.x + deltaX,
|
| 3054 |
+
y: rect.y + deltaY
|
| 3055 |
+
};
|
| 3056 |
|
| 3057 |
// Canvas境界制限
|
| 3058 |
const canvas = window.poseEditorGlobals.canvas;
|
| 3059 |
+
newRect.x = Math.max(0, Math.min(canvas.width - newRect.width, newRect.x));
|
| 3060 |
+
newRect.y = Math.max(0, Math.min(canvas.height - newRect.height, newRect.y));
|
| 3061 |
|
| 3062 |
// 矩形を更新
|
| 3063 |
+
window.poseEditorGlobals.currentRects[rectType] = newRect;
|
|
|
|
| 3064 |
|
| 3065 |
+
// キーポイントを矩形の移動に合わせて移動(refs方式)
|
| 3066 |
+
moveKeypointsWithRect(rectType, deltaX, deltaY);
|
| 3067 |
|
| 3068 |
// 🚀 refs互換:編集中もリアルタイム描画
|
| 3069 |
const currentPoseData = window.poseEditorGlobals.poseData || poseData;
|
|
|
|
| 3074 |
window.poseEditorGlobals.enableFace
|
| 3075 |
);
|
| 3076 |
}
|
| 3077 |
+
|
| 3078 |
+
// ドラッグ開始位置を更新(連続移動対応)
|
| 3079 |
+
window.poseEditorGlobals.dragStartPos = { x: mouseX, y: mouseY };
|
| 3080 |
}
|
| 3081 |
|
| 3082 |
// 🔧 矩形タイプに応じた色取得
|
|
|
|
| 3131 |
}
|
| 3132 |
}
|
| 3133 |
|
| 3134 |
+
// 💖 people形式で再取得を試行(古い構造フォールバックを削除)
|
| 3135 |
+
if (!targetKeypoints && currentPoseData.people && currentPoseData.people[0]) {
|
| 3136 |
+
const person = currentPoseData.people[0];
|
| 3137 |
+
if (rectType === 'leftHand') {
|
| 3138 |
+
targetKeypoints = person.hand_left_keypoints_2d;
|
| 3139 |
+
} else if (rectType === 'rightHand') {
|
| 3140 |
+
targetKeypoints = person.hand_right_keypoints_2d;
|
| 3141 |
+
} else if (rectType === 'face') {
|
| 3142 |
+
targetKeypoints = person.face_keypoints_2d;
|
| 3143 |
}
|
| 3144 |
}
|
| 3145 |
|
|
|
|
| 3280 |
}
|
| 3281 |
}
|
| 3282 |
|
| 3283 |
+
// 💖 people形式で再取得を試行(古い構造フォールバックを削除)
|
| 3284 |
+
if (!targetKeypoints && currentPoseData.people && currentPoseData.people[0]) {
|
| 3285 |
+
const person = currentPoseData.people[0];
|
| 3286 |
+
if (rectType === 'leftHand') {
|
| 3287 |
+
targetKeypoints = person.hand_left_keypoints_2d;
|
| 3288 |
+
} else if (rectType === 'rightHand') {
|
| 3289 |
+
targetKeypoints = person.hand_right_keypoints_2d;
|
| 3290 |
+
} else if (rectType === 'face') {
|
| 3291 |
+
targetKeypoints = person.face_keypoints_2d;
|
| 3292 |
}
|
| 3293 |
}
|
| 3294 |
|
utils/export_utils.py
CHANGED
|
@@ -5,9 +5,24 @@ import numpy as np
|
|
| 5 |
from PIL import Image, ImageDraw
|
| 6 |
import io
|
| 7 |
import base64
|
|
|
|
| 8 |
from .notifications import notify_success, notify_error
|
| 9 |
|
| 10 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
"""
|
| 12 |
ポーズデータを画像として出力
|
| 13 |
|
|
@@ -15,101 +30,199 @@ def export_pose_as_image(pose_data, canvas_size=(640, 640), background_color=(24
|
|
| 15 |
pose_data: DWPoseデータ
|
| 16 |
canvas_size: 出力画像サイズ
|
| 17 |
background_color: 背景色 (R, G, B)
|
|
|
|
|
|
|
| 18 |
|
| 19 |
Returns:
|
| 20 |
PIL.Image: ポーズ画像
|
| 21 |
"""
|
| 22 |
try:
|
|
|
|
| 23 |
if not pose_data:
|
|
|
|
| 24 |
notify_error("ポーズデータがありません")
|
| 25 |
return None
|
| 26 |
|
|
|
|
|
|
|
| 27 |
# 新しい画像を作成
|
| 28 |
image = Image.new('RGB', canvas_size, background_color)
|
| 29 |
draw = ImageDraw.Draw(image)
|
|
|
|
| 30 |
|
| 31 |
-
#
|
| 32 |
-
if '
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
-
#
|
| 36 |
-
if
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
-
#
|
| 40 |
-
if
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
|
| 43 |
-
|
|
|
|
| 44 |
return image
|
| 45 |
|
| 46 |
except Exception as e:
|
|
|
|
| 47 |
notify_error(f"ポーズ画像エクスポートに失敗しました: {str(e)}")
|
| 48 |
return None
|
| 49 |
|
| 50 |
-
def draw_body_on_image(draw,
|
| 51 |
-
"""
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
-
def draw_hands_on_image(draw, hands_data):
|
| 95 |
-
"""
|
|
|
|
| 96 |
for hand in hands_data:
|
| 97 |
if hand and len(hand) > 0:
|
| 98 |
for i in range(0, len(hand), 3):
|
| 99 |
if i + 2 < len(hand):
|
| 100 |
x, y, conf = hand[i], hand[i+1], hand[i+2]
|
| 101 |
if conf > 0.3:
|
| 102 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
|
| 104 |
-
def draw_faces_on_image(draw, faces_data):
|
| 105 |
-
"""
|
|
|
|
| 106 |
for face in faces_data:
|
| 107 |
if face and len(face) > 0:
|
| 108 |
for i in range(0, len(face), 3):
|
| 109 |
if i + 2 < len(face):
|
| 110 |
x, y, conf = face[i], face[i+1], face[i+2]
|
| 111 |
if conf > 0.3:
|
| 112 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
|
| 114 |
def export_pose_as_json(pose_data, include_metadata=True):
|
| 115 |
"""
|
|
@@ -138,7 +251,7 @@ def export_pose_as_json(pose_data, include_metadata=True):
|
|
| 138 |
}
|
| 139 |
|
| 140 |
json_str = json.dumps(export_data, indent=2, ensure_ascii=False)
|
| 141 |
-
|
| 142 |
return json_str
|
| 143 |
|
| 144 |
except Exception as e:
|
|
|
|
| 5 |
from PIL import Image, ImageDraw
|
| 6 |
import io
|
| 7 |
import base64
|
| 8 |
+
from datetime import datetime
|
| 9 |
from .notifications import notify_success, notify_error
|
| 10 |
|
| 11 |
+
def get_timestamp_filename(prefix, extension):
|
| 12 |
+
"""
|
| 13 |
+
タイムスタンプ付きファイル名を生成
|
| 14 |
+
|
| 15 |
+
Args:
|
| 16 |
+
prefix: ファイル名の前置詞
|
| 17 |
+
extension: ファイル拡張子(ドットなし)
|
| 18 |
+
|
| 19 |
+
Returns:
|
| 20 |
+
str: タイムスタンプ付きファイル名
|
| 21 |
+
"""
|
| 22 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 23 |
+
return f"{prefix}_{timestamp}.{extension}"
|
| 24 |
+
|
| 25 |
+
def export_pose_as_image(pose_data, canvas_size=(640, 640), background_color=(0, 0, 0), enable_hands=True, enable_face=True):
|
| 26 |
"""
|
| 27 |
ポーズデータを画像として出力
|
| 28 |
|
|
|
|
| 30 |
pose_data: DWPoseデータ
|
| 31 |
canvas_size: 出力画像サイズ
|
| 32 |
background_color: 背景色 (R, G, B)
|
| 33 |
+
enable_hands: 手を描画するかどうか
|
| 34 |
+
enable_face: 顔を描画するかどうか
|
| 35 |
|
| 36 |
Returns:
|
| 37 |
PIL.Image: ポーズ画像
|
| 38 |
"""
|
| 39 |
try:
|
| 40 |
+
print(f"[DEBUG] 🎨 export_pose_as_image開始 - データ: {bool(pose_data)}")
|
| 41 |
if not pose_data:
|
| 42 |
+
print(f"[DEBUG] ❌ export_pose_as_image: ポーズデータなし")
|
| 43 |
notify_error("ポーズデータがありません")
|
| 44 |
return None
|
| 45 |
|
| 46 |
+
print(f"[DEBUG] 🎨 ポーズデータ構造: {list(pose_data.keys()) if isinstance(pose_data, dict) else type(pose_data)}")
|
| 47 |
+
|
| 48 |
# 新しい画像を作成
|
| 49 |
image = Image.new('RGB', canvas_size, background_color)
|
| 50 |
draw = ImageDraw.Draw(image)
|
| 51 |
+
print(f"[DEBUG] 🎨 背景画像作成完了: {canvas_size}")
|
| 52 |
|
| 53 |
+
# ボディの描画(refs準拠)
|
| 54 |
+
if 'people' in pose_data and pose_data['people']:
|
| 55 |
+
print(f"[DEBUG] 🎨 ボディ描画開始(refs準拠)")
|
| 56 |
+
draw_body_on_image(draw, pose_data, canvas_size)
|
| 57 |
+
print(f"[DEBUG] 🎨 ボディ描画完了")
|
| 58 |
+
else:
|
| 59 |
+
print(f"[DEBUG] ⚠️ ボディデータなし - people: {'people' in pose_data}, count: {len(pose_data.get('people', []))}")
|
| 60 |
|
| 61 |
+
# 💖 手の描画(people形式とhands形式両対応)
|
| 62 |
+
if enable_hands:
|
| 63 |
+
hands_data = None
|
| 64 |
+
if 'people' in pose_data and pose_data['people'] and len(pose_data['people']) > 0:
|
| 65 |
+
person = pose_data['people'][0]
|
| 66 |
+
left_hand = person.get('hand_left_keypoints_2d', [])
|
| 67 |
+
right_hand = person.get('hand_right_keypoints_2d', [])
|
| 68 |
+
if left_hand or right_hand:
|
| 69 |
+
hands_data = [left_hand, right_hand]
|
| 70 |
+
print(f"[DEBUG] 🎨 手描画開始(people形式)- 左: {len(left_hand)}, 右: {len(right_hand)}")
|
| 71 |
+
elif 'hands' in pose_data and pose_data['hands']:
|
| 72 |
+
hands_data = pose_data['hands']
|
| 73 |
+
print(f"[DEBUG] 🎨 手描画開始(hands形式)")
|
| 74 |
+
|
| 75 |
+
if hands_data:
|
| 76 |
+
draw_hands_on_image(draw, hands_data, canvas_size)
|
| 77 |
+
print(f"[DEBUG] 🎨 手描画完了")
|
| 78 |
+
else:
|
| 79 |
+
print(f"[DEBUG] ⚠️ 手描画スキップ - 手データなし")
|
| 80 |
|
| 81 |
+
# 💖 顔の描画(people形式とfaces形式両対応)
|
| 82 |
+
if enable_face:
|
| 83 |
+
face_data = None
|
| 84 |
+
if 'people' in pose_data and pose_data['people'] and len(pose_data['people']) > 0:
|
| 85 |
+
person = pose_data['people'][0]
|
| 86 |
+
face_keypoints = person.get('face_keypoints_2d', [])
|
| 87 |
+
if face_keypoints:
|
| 88 |
+
face_data = [face_keypoints]
|
| 89 |
+
print(f"[DEBUG] 🎨 顔描画開始(people形式)- キーポイント: {len(face_keypoints)}")
|
| 90 |
+
elif 'faces' in pose_data and pose_data['faces']:
|
| 91 |
+
face_data = pose_data['faces']
|
| 92 |
+
print(f"[DEBUG] 🎨 顔描画開始(faces形式)")
|
| 93 |
+
|
| 94 |
+
if face_data:
|
| 95 |
+
draw_faces_on_image(draw, face_data, canvas_size)
|
| 96 |
+
print(f"[DEBUG] 🎨 顔描画完了")
|
| 97 |
+
else:
|
| 98 |
+
print(f"[DEBUG] ⚠️ 顔描画スキップ - 顔データなし")
|
| 99 |
|
| 100 |
+
print(f"[DEBUG] 🎨 export_pose_as_image成功!")
|
| 101 |
+
# 通知はapp.py側で行う(重複回避)
|
| 102 |
return image
|
| 103 |
|
| 104 |
except Exception as e:
|
| 105 |
+
print(f"[DEBUG] ❌ export_pose_as_image例外: {e}")
|
| 106 |
notify_error(f"ポーズ画像エクスポートに失敗しました: {str(e)}")
|
| 107 |
return None
|
| 108 |
|
| 109 |
+
def draw_body_on_image(draw, pose_data, canvas_size):
|
| 110 |
+
"""画像にボディを描画(refs準拠)"""
|
| 111 |
+
try:
|
| 112 |
+
print(f"[DEBUG] 🎨 draw_body_on_image開始(refs準拠)")
|
| 113 |
+
|
| 114 |
+
# refs準拠:peopleからpose_keypoints_2dを取得
|
| 115 |
+
people = pose_data.get("people", [])
|
| 116 |
+
if not people:
|
| 117 |
+
print(f"[DEBUG] ⚠️ people が空のため描画スキップ")
|
| 118 |
+
return
|
| 119 |
+
|
| 120 |
+
# refs準拠:接続定義
|
| 121 |
+
connections = [
|
| 122 |
+
[1, 2], [1, 5], [2, 3], [3, 4], [5, 6], [6, 7], [1, 8], [8, 9],
|
| 123 |
+
[9, 10], [1, 11], [11, 12], [12, 13], [1, 0], [0, 14], [14, 16],
|
| 124 |
+
[0, 15], [15, 17], [10, 18], [13, 19] # つま先まで含む
|
| 125 |
+
]
|
| 126 |
+
|
| 127 |
+
# refs準拠:色定義(BGR→RGB変換)
|
| 128 |
+
skeleton_colors = [
|
| 129 |
+
(0, 0, 255), (0, 85, 255), (0, 170, 255), (0, 255, 255), (0, 255, 170), (0, 255, 85), (0, 255, 0),
|
| 130 |
+
(85, 255, 0), (170, 255, 0), (255, 255, 0), (170, 255, 0), (85, 255, 0), (255, 0, 0), (255, 0, 85),
|
| 131 |
+
(255, 0, 170), (255, 0, 255), (170, 0, 255), (85, 0, 255), (255, 255, 170), (170, 255, 255)
|
| 132 |
+
]
|
| 133 |
+
|
| 134 |
+
W, H = canvas_size
|
| 135 |
+
detection_threshold = 0.3
|
| 136 |
+
|
| 137 |
+
for person in people:
|
| 138 |
+
keypoints_flat = person.get("pose_keypoints_2d", [])
|
| 139 |
+
print(f"[DEBUG] 🎨 keypoints_flat length: {len(keypoints_flat)}")
|
| 140 |
+
|
| 141 |
+
# refs準拠:3要素ずつ分割してキーポイントリスト作成
|
| 142 |
+
keypoints = []
|
| 143 |
+
for i in range(0, len(keypoints_flat), 3):
|
| 144 |
+
if i + 2 < len(keypoints_flat):
|
| 145 |
+
x, y, confidence = keypoints_flat[i:i+3]
|
| 146 |
+
keypoints.append([x, y, confidence])
|
| 147 |
+
|
| 148 |
+
print(f"[DEBUG] 🎨 keypoints count: {len(keypoints)}")
|
| 149 |
|
| 150 |
+
# 座標の正規化チェックと変換(refs準拠)
|
| 151 |
+
if len(keypoints) > 0 and all(0 <= kp[0] <= 1 and 0 <= kp[1] <= 1 for kp in keypoints if kp[2] > 0):
|
| 152 |
+
for kp in keypoints:
|
| 153 |
+
if kp[2] > 0:
|
| 154 |
+
kp[0] *= W
|
| 155 |
+
kp[1] *= H
|
| 156 |
+
|
| 157 |
+
# refs準拠:接続線の描画
|
| 158 |
+
for i, connection in enumerate(connections):
|
| 159 |
+
if i < len(skeleton_colors):
|
| 160 |
+
color = skeleton_colors[i]
|
| 161 |
+
else:
|
| 162 |
+
color = skeleton_colors[i % len(skeleton_colors)]
|
| 163 |
|
| 164 |
+
idx1, idx2 = connection
|
| 165 |
+
|
| 166 |
+
if 0 <= idx1 < len(keypoints) and 0 <= idx2 < len(keypoints):
|
| 167 |
+
kp1 = keypoints[idx1]
|
| 168 |
+
kp2 = keypoints[idx2]
|
| 169 |
+
|
| 170 |
+
if kp1[2] > detection_threshold and kp2[2] > detection_threshold:
|
| 171 |
+
# refs準拠:太い線の描画(PIL版)
|
| 172 |
+
draw.line([
|
| 173 |
+
(int(kp1[0]), int(kp1[1])),
|
| 174 |
+
(int(kp2[0]), int(kp2[1]))
|
| 175 |
+
], fill=color, width=4)
|
| 176 |
+
|
| 177 |
+
# refs準拠���キーポイントの描画
|
| 178 |
+
for i, kp in enumerate(keypoints):
|
| 179 |
+
x, y, confidence = kp
|
| 180 |
+
if confidence > detection_threshold:
|
| 181 |
+
if i < len(skeleton_colors):
|
| 182 |
+
color = skeleton_colors[i]
|
| 183 |
+
else:
|
| 184 |
+
color = skeleton_colors[i % len(skeleton_colors)]
|
| 185 |
+
|
| 186 |
+
draw.ellipse([int(x)-4, int(y)-4, int(x)+4, int(y)+4], fill=color)
|
| 187 |
+
|
| 188 |
+
print(f"[DEBUG] 🎨 draw_body_on_image完了")
|
| 189 |
+
|
| 190 |
+
except Exception as e:
|
| 191 |
+
print(f"[DEBUG] ❌ draw_body_on_image例外: {e}")
|
| 192 |
+
import traceback
|
| 193 |
+
traceback.print_exc()
|
| 194 |
|
| 195 |
+
def draw_hands_on_image(draw, hands_data, canvas_size):
|
| 196 |
+
"""💖 画像に手を描画(座標変換対応)"""
|
| 197 |
+
W, H = canvas_size
|
| 198 |
for hand in hands_data:
|
| 199 |
if hand and len(hand) > 0:
|
| 200 |
for i in range(0, len(hand), 3):
|
| 201 |
if i + 2 < len(hand):
|
| 202 |
x, y, conf = hand[i], hand[i+1], hand[i+2]
|
| 203 |
if conf > 0.3:
|
| 204 |
+
# 💖 座標の正規化チェック(0-1の場合はCanvas座標に変換)
|
| 205 |
+
if 0 <= x <= 1 and 0 <= y <= 1:
|
| 206 |
+
x = x * W
|
| 207 |
+
y = y * H
|
| 208 |
+
# refs準拠: OpenCV(255,0,0)BGR → PIL(0,0,255)RGB = 青
|
| 209 |
+
draw.ellipse([int(x)-3, int(y)-3, int(x)+3, int(y)+3], fill=(0, 0, 255))
|
| 210 |
|
| 211 |
+
def draw_faces_on_image(draw, faces_data, canvas_size):
|
| 212 |
+
"""💖 画像に顔を描画(座標変換対応)"""
|
| 213 |
+
W, H = canvas_size
|
| 214 |
for face in faces_data:
|
| 215 |
if face and len(face) > 0:
|
| 216 |
for i in range(0, len(face), 3):
|
| 217 |
if i + 2 < len(face):
|
| 218 |
x, y, conf = face[i], face[i+1], face[i+2]
|
| 219 |
if conf > 0.3:
|
| 220 |
+
# 💖 座標の正規化チェック(0-1の場合はCanvas座標に変換)
|
| 221 |
+
if 0 <= x <= 1 and 0 <= y <= 1:
|
| 222 |
+
x = x * W
|
| 223 |
+
y = y * H
|
| 224 |
+
# refs準拠: OpenCV(255,255,255)BGR → PIL(255,255,255)RGB = 白
|
| 225 |
+
draw.ellipse([int(x)-2, int(y)-2, int(x)+2, int(y)+2], fill=(255, 255, 255))
|
| 226 |
|
| 227 |
def export_pose_as_json(pose_data, include_metadata=True):
|
| 228 |
"""
|
|
|
|
| 251 |
}
|
| 252 |
|
| 253 |
json_str = json.dumps(export_data, indent=2, ensure_ascii=False)
|
| 254 |
+
# 通知はapp.py側で行う(重複回避)
|
| 255 |
return json_str
|
| 256 |
|
| 257 |
except Exception as e:
|