feat: JSONフォーマットをpeople形式に統一
Browse files- export_utils.py: export_pose_as_json()をpeople形式出力に変更
- app.py: JSON読み込み処理でpeople形式に対応
- 既存のbodies形式との互換性を維持
- canvas_width/canvas_heightをサポート
- データ変換処理を簡素化してパフォーマンス向上
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- app.py +80 -45
- utils/export_utils.py +68 -13
app.py
CHANGED
|
@@ -462,47 +462,25 @@ def main():
|
|
| 462 |
return gr.update(visible=False)
|
| 463 |
|
| 464 |
def export_json(pose_data):
|
| 465 |
-
"""ポーズJSONをエクスポート(
|
| 466 |
global _current_poses, _current_frame_index
|
| 467 |
|
| 468 |
-
# refsと同じ_current_poses
|
| 469 |
export_data = None
|
| 470 |
if _current_poses and 0 <= _current_frame_index < len(_current_poses):
|
| 471 |
current_frame = _current_poses[_current_frame_index]
|
| 472 |
if current_frame['people'] and current_frame['people'][0]:
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
# people形式からbodies.candidate形式に変換(エクスポート用)
|
| 476 |
export_data = {
|
| 477 |
-
'
|
| 478 |
-
'hands': [],
|
| 479 |
-
'faces': [],
|
| 480 |
'resolution': current_frame['metadata'].get('resolution', [512, 512])
|
| 481 |
}
|
| 482 |
-
|
| 483 |
-
# pose_keypoints_2d をcandidate形式に変換
|
| 484 |
-
if 'pose_keypoints_2d' in person:
|
| 485 |
-
pose_keypoints = person['pose_keypoints_2d']
|
| 486 |
-
for i in range(0, len(pose_keypoints), 3):
|
| 487 |
-
if i + 2 < len(pose_keypoints):
|
| 488 |
-
x = pose_keypoints[i]
|
| 489 |
-
y = pose_keypoints[i + 1]
|
| 490 |
-
conf = pose_keypoints[i + 2]
|
| 491 |
-
export_data['bodies']['candidate'].append([x, y, conf, 0])
|
| 492 |
-
|
| 493 |
-
# 手と顔データ
|
| 494 |
-
left_hand = person.get('hand_left_keypoints_2d', [])
|
| 495 |
-
right_hand = person.get('hand_right_keypoints_2d', [])
|
| 496 |
-
face_data = person.get('face_keypoints_2d', [])
|
| 497 |
-
|
| 498 |
-
export_data['hands'] = [left_hand, right_hand]
|
| 499 |
-
export_data['faces'] = [face_data] if face_data else []
|
| 500 |
|
| 501 |
# フォールバック:引数のpose_dataを使用
|
| 502 |
if export_data is None:
|
| 503 |
export_data = pose_data
|
| 504 |
|
| 505 |
-
print(f"[DEBUG] 📥 JSON
|
| 506 |
print(f"[DEBUG] 📊 グローバルデータ: {bool(_current_poses)}, 引数データ: {bool(pose_data)}")
|
| 507 |
print(f"[DEBUG] 📊 使用データ: {bool(export_data)}")
|
| 508 |
|
|
@@ -512,6 +490,7 @@ def main():
|
|
| 512 |
return gr.update(visible=False)
|
| 513 |
|
| 514 |
try:
|
|
|
|
| 515 |
json_str = export_pose_as_json(export_data)
|
| 516 |
if not json_str:
|
| 517 |
print(f"[DEBUG] ❌ JSON生成失敗")
|
|
@@ -525,7 +504,7 @@ def main():
|
|
| 525 |
f.write(json_str)
|
| 526 |
|
| 527 |
print(f"[DEBUG] ✅ ファイル準備完了: {temp_path}")
|
| 528 |
-
notify_success(f"JSONをエクスポートしました: {filename}")
|
| 529 |
|
| 530 |
# 🎯 Button + File方式でファイルを表示
|
| 531 |
return gr.update(value=temp_path, visible=True)
|
|
@@ -665,7 +644,7 @@ def main():
|
|
| 665 |
print(f"[DEBUG] 🔓 データ更新処理完了 - フラグ解除: {old_flag} → {_is_updating} (timestamp={update_timestamp})")
|
| 666 |
|
| 667 |
def on_json_upload(file):
|
| 668 |
-
"""JSONファイルアップロード時の処理(refs
|
| 669 |
global _current_poses, _current_frame_index
|
| 670 |
|
| 671 |
if file is None:
|
|
@@ -679,18 +658,46 @@ def main():
|
|
| 679 |
# JSONをパース
|
| 680 |
loaded_data = json.loads(json_content)
|
| 681 |
|
| 682 |
-
#
|
| 683 |
-
if
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 694 |
person_data = {
|
| 695 |
"pose_keypoints_2d": [],
|
| 696 |
"hand_left_keypoints_2d": loaded_data.get('hands', [[], []])[0] if loaded_data.get('hands') else [],
|
|
@@ -712,10 +719,9 @@ def main():
|
|
| 712 |
}
|
| 713 |
}]
|
| 714 |
_current_frame_index = 0
|
| 715 |
-
print(f"[DEBUG] ✅
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
return loaded_data, loaded_data
|
| 719 |
|
| 720 |
except json.JSONDecodeError as e:
|
| 721 |
notify_error(f"JSONパースエラー: {str(e)}")
|
|
@@ -723,6 +729,35 @@ def main():
|
|
| 723 |
except Exception as e:
|
| 724 |
notify_error(f"ファイル読み込みエラー: {str(e)}")
|
| 725 |
return None, {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 726 |
|
| 727 |
def validate_pose_json(data):
|
| 728 |
"""ポーズJSONデータの検証"""
|
|
|
|
| 462 |
return gr.update(visible=False)
|
| 463 |
|
| 464 |
def export_json(pose_data):
|
| 465 |
+
"""ポーズJSONをエクスポート(people形式・refs互換・グローバル管理)"""
|
| 466 |
global _current_poses, _current_frame_index
|
| 467 |
|
| 468 |
+
# refsと同じ_current_posesから最新データを取得(簡素化)
|
| 469 |
export_data = None
|
| 470 |
if _current_poses and 0 <= _current_frame_index < len(_current_poses):
|
| 471 |
current_frame = _current_poses[_current_frame_index]
|
| 472 |
if current_frame['people'] and current_frame['people'][0]:
|
| 473 |
+
# 💖 people形式で直接エクスポート(変換不要)
|
|
|
|
|
|
|
| 474 |
export_data = {
|
| 475 |
+
'people': current_frame['people'],
|
|
|
|
|
|
|
| 476 |
'resolution': current_frame['metadata'].get('resolution', [512, 512])
|
| 477 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 478 |
|
| 479 |
# フォールバック:引数のpose_dataを使用
|
| 480 |
if export_data is None:
|
| 481 |
export_data = pose_data
|
| 482 |
|
| 483 |
+
print(f"[DEBUG] 📥 JSONダウンロードボタンクリック(people形式)")
|
| 484 |
print(f"[DEBUG] 📊 グローバルデータ: {bool(_current_poses)}, 引数データ: {bool(pose_data)}")
|
| 485 |
print(f"[DEBUG] 📊 使用データ: {bool(export_data)}")
|
| 486 |
|
|
|
|
| 490 |
return gr.update(visible=False)
|
| 491 |
|
| 492 |
try:
|
| 493 |
+
# export_pose_as_json()が新しいpeople形式で出力
|
| 494 |
json_str = export_pose_as_json(export_data)
|
| 495 |
if not json_str:
|
| 496 |
print(f"[DEBUG] ❌ JSON生成失敗")
|
|
|
|
| 504 |
f.write(json_str)
|
| 505 |
|
| 506 |
print(f"[DEBUG] ✅ ファイル準備完了: {temp_path}")
|
| 507 |
+
notify_success(f"people形式JSONをエクスポートしました: {filename}")
|
| 508 |
|
| 509 |
# 🎯 Button + File方式でファイルを表示
|
| 510 |
return gr.update(value=temp_path, visible=True)
|
|
|
|
| 644 |
print(f"[DEBUG] 🔓 データ更新処理完了 - フラグ解除: {old_flag} → {_is_updating} (timestamp={update_timestamp})")
|
| 645 |
|
| 646 |
def on_json_upload(file):
|
| 647 |
+
"""JSONファイルアップロード時の処理(people形式対応・refs互換)"""
|
| 648 |
global _current_poses, _current_frame_index
|
| 649 |
|
| 650 |
if file is None:
|
|
|
|
| 658 |
# JSONをパース
|
| 659 |
loaded_data = json.loads(json_content)
|
| 660 |
|
| 661 |
+
# people形式(配列)かどうかチェック
|
| 662 |
+
if isinstance(loaded_data, list) and len(loaded_data) > 0:
|
| 663 |
+
# 新しいpeople形式
|
| 664 |
+
frame_data = loaded_data[0] # 最初のフレームを使用
|
| 665 |
+
|
| 666 |
+
if 'people' in frame_data and frame_data['people']:
|
| 667 |
+
person_data = frame_data['people'][0]
|
| 668 |
+
canvas_width = frame_data.get('canvas_width', 512)
|
| 669 |
+
canvas_height = frame_data.get('canvas_height', 512)
|
| 670 |
+
|
| 671 |
+
# グローバル変数に保存(refs互換)
|
| 672 |
+
_current_poses = [{
|
| 673 |
+
'people': [person_data],
|
| 674 |
+
'metadata': {
|
| 675 |
+
'resolution': [canvas_width, canvas_height]
|
| 676 |
+
}
|
| 677 |
+
}]
|
| 678 |
+
_current_frame_index = 0
|
| 679 |
+
|
| 680 |
+
# 表示用データ構築(互換性維持)
|
| 681 |
+
display_data = convert_people_to_bodies_format(person_data, [canvas_width, canvas_height])
|
| 682 |
+
|
| 683 |
+
print(f"[DEBUG] ✅ people形式JSON読み込み完了")
|
| 684 |
+
notify_success("people形式JSONファイルを読み込みました")
|
| 685 |
+
return display_data, display_data
|
| 686 |
+
else:
|
| 687 |
+
notify_error("無効なpeople形式データです")
|
| 688 |
+
return None, {}
|
| 689 |
+
|
| 690 |
+
else:
|
| 691 |
+
# 従来のbodies形式(互換性維持)
|
| 692 |
+
if not validate_pose_json(loaded_data):
|
| 693 |
+
notify_error("無効なポーズデータフォーマットです")
|
| 694 |
+
return None, {}
|
| 695 |
+
|
| 696 |
+
# メタデータを除去(存在する場合)
|
| 697 |
+
if 'metadata' in loaded_data:
|
| 698 |
+
del loaded_data['metadata']
|
| 699 |
+
|
| 700 |
+
# bodies形式からpeople形式に変換してグローバル保存
|
| 701 |
person_data = {
|
| 702 |
"pose_keypoints_2d": [],
|
| 703 |
"hand_left_keypoints_2d": loaded_data.get('hands', [[], []])[0] if loaded_data.get('hands') else [],
|
|
|
|
| 719 |
}
|
| 720 |
}]
|
| 721 |
_current_frame_index = 0
|
| 722 |
+
print(f"[DEBUG] ✅ bodies形式JSON読み込み・変換完了(互換性維持)")
|
| 723 |
+
notify_success("bodies形式JSONファイルを読み込みました(people形式に変換)")
|
| 724 |
+
return loaded_data, loaded_data
|
|
|
|
| 725 |
|
| 726 |
except json.JSONDecodeError as e:
|
| 727 |
notify_error(f"JSONパースエラー: {str(e)}")
|
|
|
|
| 729 |
except Exception as e:
|
| 730 |
notify_error(f"ファイル読み込みエラー: {str(e)}")
|
| 731 |
return None, {}
|
| 732 |
+
|
| 733 |
+
def convert_people_to_bodies_format(person_data, resolution):
|
| 734 |
+
"""people形式からbodies形式に変換(表示互換性用)"""
|
| 735 |
+
bodies_data = {
|
| 736 |
+
'bodies': {'candidate': [], 'subset': []},
|
| 737 |
+
'hands': [],
|
| 738 |
+
'faces': [],
|
| 739 |
+
'resolution': resolution
|
| 740 |
+
}
|
| 741 |
+
|
| 742 |
+
# pose_keypoints_2d をcandidate形式に変換
|
| 743 |
+
if 'pose_keypoints_2d' in person_data:
|
| 744 |
+
pose_keypoints = person_data['pose_keypoints_2d']
|
| 745 |
+
for i in range(0, len(pose_keypoints), 3):
|
| 746 |
+
if i + 2 < len(pose_keypoints):
|
| 747 |
+
x = pose_keypoints[i]
|
| 748 |
+
y = pose_keypoints[i + 1]
|
| 749 |
+
conf = pose_keypoints[i + 2]
|
| 750 |
+
bodies_data['bodies']['candidate'].append([x, y, conf, 0])
|
| 751 |
+
|
| 752 |
+
# 手と顔データ
|
| 753 |
+
left_hand = person_data.get('hand_left_keypoints_2d', [])
|
| 754 |
+
right_hand = person_data.get('hand_right_keypoints_2d', [])
|
| 755 |
+
face_data = person_data.get('face_keypoints_2d', [])
|
| 756 |
+
|
| 757 |
+
bodies_data['hands'] = [left_hand, right_hand]
|
| 758 |
+
bodies_data['faces'] = [face_data] if face_data else []
|
| 759 |
+
|
| 760 |
+
return bodies_data
|
| 761 |
|
| 762 |
def validate_pose_json(data):
|
| 763 |
"""ポーズJSONデータの検証"""
|
utils/export_utils.py
CHANGED
|
@@ -224,31 +224,86 @@ def draw_faces_on_image(draw, faces_data, canvas_size):
|
|
| 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=
|
| 228 |
"""
|
| 229 |
-
ポーズデータをJSONとして出力
|
| 230 |
|
| 231 |
Args:
|
| 232 |
-
pose_data: DWPose
|
| 233 |
-
include_metadata:
|
| 234 |
|
| 235 |
Returns:
|
| 236 |
-
str: JSON文字列
|
| 237 |
"""
|
| 238 |
try:
|
| 239 |
if not pose_data:
|
| 240 |
notify_error("ポーズデータがありません")
|
| 241 |
return None
|
| 242 |
|
| 243 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
|
| 245 |
-
|
| 246 |
-
export_data['metadata'] = {
|
| 247 |
-
'format': 'dwpose-editor',
|
| 248 |
-
'version': '1.0',
|
| 249 |
-
'exported_at': str(np.datetime64('now')),
|
| 250 |
-
'description': '2頭身・3頭身キャラクター用ポーズデータ'
|
| 251 |
-
}
|
| 252 |
|
| 253 |
json_str = json.dumps(export_data, indent=2, ensure_ascii=False)
|
| 254 |
# 通知はapp.py側で行う(重複回避)
|
|
|
|
| 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=False):
|
| 228 |
"""
|
| 229 |
+
ポーズデータをpeople形式のJSONとして出力
|
| 230 |
|
| 231 |
Args:
|
| 232 |
+
pose_data: DWPoseデータ(people形式またはbodies形式)
|
| 233 |
+
include_metadata: メタデータを含めるかどうか(デフォルト: False)
|
| 234 |
|
| 235 |
Returns:
|
| 236 |
+
str: people形式のJSON文字列
|
| 237 |
"""
|
| 238 |
try:
|
| 239 |
if not pose_data:
|
| 240 |
notify_error("ポーズデータがありません")
|
| 241 |
return None
|
| 242 |
|
| 243 |
+
# people形式の出力データ構造を作成
|
| 244 |
+
export_data = []
|
| 245 |
+
|
| 246 |
+
# デフォルトの解像度
|
| 247 |
+
canvas_width = 512
|
| 248 |
+
canvas_height = 512
|
| 249 |
+
|
| 250 |
+
# pose_dataから解像度情報を取得
|
| 251 |
+
if 'resolution' in pose_data and pose_data['resolution']:
|
| 252 |
+
resolution = pose_data['resolution']
|
| 253 |
+
if isinstance(resolution, list) and len(resolution) >= 2:
|
| 254 |
+
canvas_width = int(resolution[0])
|
| 255 |
+
canvas_height = int(resolution[1])
|
| 256 |
+
elif 'metadata' in pose_data and 'resolution' in pose_data['metadata']:
|
| 257 |
+
resolution = pose_data['metadata']['resolution']
|
| 258 |
+
if isinstance(resolution, list) and len(resolution) >= 2:
|
| 259 |
+
canvas_width = int(resolution[0])
|
| 260 |
+
canvas_height = int(resolution[1])
|
| 261 |
+
|
| 262 |
+
# people形式データの構築
|
| 263 |
+
person_data = {
|
| 264 |
+
"pose_keypoints_2d": [],
|
| 265 |
+
"face_keypoints_2d": [],
|
| 266 |
+
"hand_left_keypoints_2d": [],
|
| 267 |
+
"hand_right_keypoints_2d": []
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
# people形式が既に存在する場合はそのまま使用
|
| 271 |
+
if 'people' in pose_data and pose_data['people']:
|
| 272 |
+
person_data = pose_data['people'][0].copy()
|
| 273 |
+
else:
|
| 274 |
+
# bodies形式からpeople形式に変換
|
| 275 |
+
if 'bodies' in pose_data and 'candidate' in pose_data['bodies']:
|
| 276 |
+
candidates = pose_data['bodies']['candidate']
|
| 277 |
+
for candidate in candidates:
|
| 278 |
+
if candidate and len(candidate) >= 2:
|
| 279 |
+
person_data["pose_keypoints_2d"].extend([
|
| 280 |
+
candidate[0],
|
| 281 |
+
candidate[1],
|
| 282 |
+
candidate[2] if len(candidate) > 2 else 1.0
|
| 283 |
+
])
|
| 284 |
+
|
| 285 |
+
# 手データ
|
| 286 |
+
if 'hands' in pose_data and pose_data['hands']:
|
| 287 |
+
hands = pose_data['hands']
|
| 288 |
+
if len(hands) > 0:
|
| 289 |
+
person_data["hand_left_keypoints_2d"] = hands[0] if hands[0] else []
|
| 290 |
+
if len(hands) > 1:
|
| 291 |
+
person_data["hand_right_keypoints_2d"] = hands[1] if hands[1] else []
|
| 292 |
+
|
| 293 |
+
# 顔データ
|
| 294 |
+
if 'faces' in pose_data and pose_data['faces']:
|
| 295 |
+
faces = pose_data['faces']
|
| 296 |
+
if len(faces) > 0:
|
| 297 |
+
person_data["face_keypoints_2d"] = faces[0] if faces[0] else []
|
| 298 |
+
|
| 299 |
+
# フレームデータの構築
|
| 300 |
+
frame_data = {
|
| 301 |
+
"people": [person_data],
|
| 302 |
+
"canvas_width": canvas_width,
|
| 303 |
+
"canvas_height": canvas_height
|
| 304 |
+
}
|
| 305 |
|
| 306 |
+
export_data.append(frame_data)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
|
| 308 |
json_str = json.dumps(export_data, indent=2, ensure_ascii=False)
|
| 309 |
# 通知はapp.py側で行う(重複回避)
|