feat(issue-045): Canvas aspect sync + data scaling + export fixes\n\n- Canvas display follows output aspect (long-side 640)\n- Align poseData resolution to output form on load\n- Keep JS->Python resolution sync; fix background fit\n- Export uses declared resolution; safe fallback\n\nClose: issue_045_キャンバスサイズ動的更新機能実装.md
Browse files- app.py +105 -16
- static/pose_editor.js +497 -183
- utils/export_utils.py +103 -11
app.py
CHANGED
|
@@ -142,6 +142,11 @@ def main():
|
|
| 142 |
scale=1,
|
| 143 |
min_width=100
|
| 144 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
|
| 146 |
# ポーズ画像出力(非表示)
|
| 147 |
output_image = gr.Image(
|
|
@@ -234,17 +239,42 @@ def main():
|
|
| 234 |
_current_frame_index = 0
|
| 235 |
print(f"[DEBUG] ✅ グローバル変数更新完了(画像アップロード・refs互換)")
|
| 236 |
|
| 237 |
-
# 🎨
|
| 238 |
image_base64 = image_to_base64(image)
|
| 239 |
pose_result['background_image'] = image_base64
|
| 240 |
|
| 241 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
else:
|
| 243 |
print(f"[DEBUG] ❌ Pose detection failed")
|
| 244 |
-
return None, {}, gr.update()
|
| 245 |
|
| 246 |
def on_canvas_size_update(width, height):
|
| 247 |
"""Canvas解像度更新"""
|
|
|
|
| 248 |
try:
|
| 249 |
width = int(width) if width else 512
|
| 250 |
height = int(height) if height else 512
|
|
@@ -256,6 +286,13 @@ def main():
|
|
| 256 |
# 座標系更新
|
| 257 |
update_coordinate_system((width, height), (640, 640))
|
| 258 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 259 |
# JavaScript側でCanvas更新
|
| 260 |
js_code = f"updateCanvasResolution({width}, {height});"
|
| 261 |
|
|
@@ -357,15 +394,33 @@ def main():
|
|
| 357 |
'is_template_load': True # テンプレート読み込みフラグ
|
| 358 |
}
|
| 359 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 360 |
notify_success(f"{template_name}を読み込みました")
|
| 361 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 362 |
else:
|
| 363 |
notify_error("テンプレートが見つかりません")
|
| 364 |
-
return None, {}
|
| 365 |
|
| 366 |
except Exception as e:
|
| 367 |
notify_error(f"テンプレート読み込みに失敗しました: {str(e)}")
|
| 368 |
-
return None, {}
|
| 369 |
|
| 370 |
def export_image(pose_data, draw_hand, draw_face, width, height):
|
| 371 |
"""ポーズ画像をエクスポート(Button + File方式)(refs互換・マルチフレーム管理)"""
|
|
@@ -577,6 +632,12 @@ def main():
|
|
| 577 |
# 現在のフレームのpeopleデータを更新(refs互換)
|
| 578 |
if 0 <= _current_frame_index < len(_current_poses):
|
| 579 |
_current_poses[_current_frame_index]['people'] = [pose_data]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 580 |
print(f"[DEBUG] 🎯 フレーム{_current_frame_index}のpeopleデータ更新完了")
|
| 581 |
else:
|
| 582 |
# フレームが範囲外の場合は追加
|
|
@@ -648,7 +709,7 @@ def main():
|
|
| 648 |
global _current_poses, _current_frame_index
|
| 649 |
|
| 650 |
if file is None:
|
| 651 |
-
return None, {}
|
| 652 |
|
| 653 |
try:
|
| 654 |
# ファイルを読み込む
|
|
@@ -690,10 +751,18 @@ def main():
|
|
| 690 |
|
| 691 |
print(f"[DEBUG] ✅ people形式JSON読み込み完了(ハイブリッド形式)")
|
| 692 |
notify_success("people形式JSONファイルを読み込みました")
|
| 693 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 694 |
else:
|
| 695 |
notify_error("無効なpeople形式データです")
|
| 696 |
-
return None, {}
|
| 697 |
|
| 698 |
else:
|
| 699 |
# 従来のbodies形式(互換性維持)
|
|
@@ -734,14 +803,27 @@ def main():
|
|
| 734 |
|
| 735 |
print(f"[DEBUG] ✅ bodies形式JSON読み込み・変換完了(ハイブリッド形式)")
|
| 736 |
notify_success("bodies形式JSONファイルを読み込みました(people形式に変換)")
|
| 737 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 738 |
|
| 739 |
except json.JSONDecodeError as e:
|
| 740 |
notify_error(f"JSONパースエラー: {str(e)}")
|
| 741 |
-
return None, {}
|
| 742 |
except Exception as e:
|
| 743 |
notify_error(f"ファイル読み込みエラー: {str(e)}")
|
| 744 |
-
return None, {}
|
| 745 |
|
| 746 |
def convert_people_to_bodies_format(person_data, resolution):
|
| 747 |
"""people形式からbodies形��に変換(表示互換性用)"""
|
|
@@ -800,7 +882,7 @@ def main():
|
|
| 800 |
input_image.change(
|
| 801 |
fn=on_image_upload,
|
| 802 |
inputs=[input_image],
|
| 803 |
-
outputs=[output_json, pose_data, js_executor]
|
| 804 |
)
|
| 805 |
|
| 806 |
# pose_data変更時にCanvas更新(重要!)- 無限ループ防止
|
|
@@ -835,7 +917,7 @@ def main():
|
|
| 835 |
template_update_btn.click(
|
| 836 |
fn=load_template_pose,
|
| 837 |
inputs=[template_dropdown],
|
| 838 |
-
outputs=[output_json, pose_data]
|
| 839 |
)
|
| 840 |
|
| 841 |
# エクスポートイベント (refs互換DownloadButton方式)
|
|
@@ -862,11 +944,18 @@ def main():
|
|
| 862 |
json_upload.change(
|
| 863 |
fn=on_json_upload,
|
| 864 |
inputs=[json_upload],
|
| 865 |
-
outputs=[output_json, pose_data]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 866 |
)
|
| 867 |
|
| 868 |
return demo
|
| 869 |
|
| 870 |
if __name__ == "__main__":
|
| 871 |
demo = main()
|
| 872 |
-
demo.launch()
|
|
|
|
| 142 |
scale=1,
|
| 143 |
min_width=100
|
| 144 |
)
|
| 145 |
+
# サイズ適用ボタン(手動更新トリガー)
|
| 146 |
+
canvas_update_btn = gr.Button(
|
| 147 |
+
"画像サイズのUpdate",
|
| 148 |
+
variant="secondary"
|
| 149 |
+
)
|
| 150 |
|
| 151 |
# ポーズ画像出力(非表示)
|
| 152 |
output_image = gr.Image(
|
|
|
|
| 239 |
_current_frame_index = 0
|
| 240 |
print(f"[DEBUG] ✅ グローバル変数更新完了(画像アップロード・refs互換)")
|
| 241 |
|
| 242 |
+
# 🎨 背景画像をポーズデータに含める(元画像をそのまま)
|
| 243 |
image_base64 = image_to_base64(image)
|
| 244 |
pose_result['background_image'] = image_base64
|
| 245 |
|
| 246 |
+
# 🔧 Issue045: 参考画像サイズに合わせてCanvas/出力サイズを更新
|
| 247 |
+
try:
|
| 248 |
+
# original_size: (width, height)
|
| 249 |
+
data_w = int(original_size[0]) if original_size else 512
|
| 250 |
+
data_h = int(original_size[1]) if original_size else 512
|
| 251 |
+
# Canvas表示サイズ(希望の出力サイズ)
|
| 252 |
+
new_w = max(64, min(2048, data_w))
|
| 253 |
+
new_h = max(64, min(2048, data_h))
|
| 254 |
+
# 重要: pose_result['resolution'] は検出結果そのまま(座標系)を保持し、JS側でスケールする
|
| 255 |
+
# Python側のメタは検出のまま(_current_poses 作成時に設定済み)
|
| 256 |
+
except Exception:
|
| 257 |
+
data_w, data_h = 512, 512
|
| 258 |
+
new_w = 512
|
| 259 |
+
new_h = 512
|
| 260 |
+
|
| 261 |
+
# JSでCanvasとデータを新サイズへスケール(左寄り防止のため)
|
| 262 |
+
js_code = f"setTimeout(() => updateCanvasResolution({new_w}, {new_h}), 100);"
|
| 263 |
+
|
| 264 |
+
return (
|
| 265 |
+
pose_result, # output_json
|
| 266 |
+
pose_result, # pose_data
|
| 267 |
+
gr.update(value=f"<script>{js_code}</script>"), # js_executor (canvas表示は正方形)
|
| 268 |
+
gr.update(value=new_w), # 出力幅(データ解像度)
|
| 269 |
+
gr.update(value=new_h) # 出力高さ(データ解像度)
|
| 270 |
+
)
|
| 271 |
else:
|
| 272 |
print(f"[DEBUG] ❌ Pose detection failed")
|
| 273 |
+
return None, {}, gr.update(), gr.update(), gr.update()
|
| 274 |
|
| 275 |
def on_canvas_size_update(width, height):
|
| 276 |
"""Canvas解像度更新"""
|
| 277 |
+
global _current_poses, _current_frame_index
|
| 278 |
try:
|
| 279 |
width = int(width) if width else 512
|
| 280 |
height = int(height) if height else 512
|
|
|
|
| 286 |
# 座標系更新
|
| 287 |
update_coordinate_system((width, height), (640, 640))
|
| 288 |
|
| 289 |
+
# Python側のメタ解像度も更新(エクスポート整合性)
|
| 290 |
+
try:
|
| 291 |
+
if _current_poses and 0 <= _current_frame_index < len(_current_poses):
|
| 292 |
+
_current_poses[_current_frame_index]['metadata']['resolution'] = [width, height]
|
| 293 |
+
except Exception:
|
| 294 |
+
pass
|
| 295 |
+
|
| 296 |
# JavaScript側でCanvas更新
|
| 297 |
js_code = f"updateCanvasResolution({width}, {height});"
|
| 298 |
|
|
|
|
| 394 |
'is_template_load': True # テンプレート読み込みフラグ
|
| 395 |
}
|
| 396 |
|
| 397 |
+
# 🔧 Issue045: テンプレートは常に512x512へ
|
| 398 |
+
target_w, target_h = 512, 512
|
| 399 |
+
# Python側メタも512固定に
|
| 400 |
+
try:
|
| 401 |
+
if _current_poses and 0 <= _current_frame_index < len(_current_poses):
|
| 402 |
+
_current_poses[_current_frame_index]['metadata']['resolution'] = [target_w, target_h]
|
| 403 |
+
except Exception:
|
| 404 |
+
pass
|
| 405 |
+
|
| 406 |
+
# JS実行とUIサイズ反映
|
| 407 |
+
js_code = f"setTimeout(() => updateCanvasResolution({target_w}, {target_h}), 100);"
|
| 408 |
+
|
| 409 |
notify_success(f"{template_name}を読み込みました")
|
| 410 |
+
return (
|
| 411 |
+
people_format_data, # output_json
|
| 412 |
+
people_format_data, # pose_data
|
| 413 |
+
gr.update(value=f"<script>{js_code}</script>"), # js_executor
|
| 414 |
+
gr.update(value=target_w), # canvas_width
|
| 415 |
+
gr.update(value=target_h) # canvas_height
|
| 416 |
+
)
|
| 417 |
else:
|
| 418 |
notify_error("テンプレートが見つかりません")
|
| 419 |
+
return None, {}, gr.update(), gr.update(), gr.update()
|
| 420 |
|
| 421 |
except Exception as e:
|
| 422 |
notify_error(f"テンプレート読み込みに失敗しました: {str(e)}")
|
| 423 |
+
return None, {}, gr.update(), gr.update(), gr.update()
|
| 424 |
|
| 425 |
def export_image(pose_data, draw_hand, draw_face, width, height):
|
| 426 |
"""ポーズ画像をエクスポート(Button + File方式)(refs互換・マルチフレーム管理)"""
|
|
|
|
| 632 |
# 現在のフレームのpeopleデータを更新(refs互換)
|
| 633 |
if 0 <= _current_frame_index < len(_current_poses):
|
| 634 |
_current_poses[_current_frame_index]['people'] = [pose_data]
|
| 635 |
+
# 🔧 JS側で解像度が更新されている場合はmetadataにも反映
|
| 636 |
+
try:
|
| 637 |
+
if 'resolution' in canvas_data and isinstance(canvas_data['resolution'], list):
|
| 638 |
+
_current_poses[_current_frame_index]['metadata']['resolution'] = canvas_data['resolution']
|
| 639 |
+
except Exception:
|
| 640 |
+
pass
|
| 641 |
print(f"[DEBUG] 🎯 フレーム{_current_frame_index}のpeopleデータ更新完了")
|
| 642 |
else:
|
| 643 |
# フレームが範囲外の場合は追加
|
|
|
|
| 709 |
global _current_poses, _current_frame_index
|
| 710 |
|
| 711 |
if file is None:
|
| 712 |
+
return None, {}, gr.update(), gr.update(), gr.update()
|
| 713 |
|
| 714 |
try:
|
| 715 |
# ファイルを読み込む
|
|
|
|
| 751 |
|
| 752 |
print(f"[DEBUG] ✅ people形式JSON読み込み完了(ハイブリッド形式)")
|
| 753 |
notify_success("people形式JSONファイルを読み込みました")
|
| 754 |
+
# CanvasとデータをJSON内の解像度に合わせる
|
| 755 |
+
js_code = f"setTimeout(() => updateCanvasResolution({int(canvas_width)}, {int(canvas_height)}), 100);"
|
| 756 |
+
return (
|
| 757 |
+
display_data, # output_json
|
| 758 |
+
display_data, # pose_data
|
| 759 |
+
gr.update(value=f"<script>{js_code}</script>"), # js_executor
|
| 760 |
+
gr.update(value=int(canvas_width)), # 出力幅
|
| 761 |
+
gr.update(value=int(canvas_height)) # 出力高さ
|
| 762 |
+
)
|
| 763 |
else:
|
| 764 |
notify_error("無効なpeople形式データです")
|
| 765 |
+
return None, {}, gr.update(), gr.update(), gr.update()
|
| 766 |
|
| 767 |
else:
|
| 768 |
# 従来のbodies形式(互換性維持)
|
|
|
|
| 803 |
|
| 804 |
print(f"[DEBUG] ✅ bodies形式JSON読み込み・変換完了(ハイブリッド形式)")
|
| 805 |
notify_success("bodies形式JSONファイルを読み込みました(people形式に変換)")
|
| 806 |
+
# 幅・高さを決定
|
| 807 |
+
res = loaded_data.get('resolution', [512, 512])
|
| 808 |
+
try:
|
| 809 |
+
w = int(res[0]); h = int(res[1])
|
| 810 |
+
except Exception:
|
| 811 |
+
w, h = 512, 512
|
| 812 |
+
js_code = f"setTimeout(() => updateCanvasResolution({w}, {h}), 100);"
|
| 813 |
+
return (
|
| 814 |
+
hybrid_data, # output_json
|
| 815 |
+
hybrid_data, # pose_data
|
| 816 |
+
gr.update(value=f"<script>{js_code}</script>"),
|
| 817 |
+
gr.update(value=w),
|
| 818 |
+
gr.update(value=h)
|
| 819 |
+
)
|
| 820 |
|
| 821 |
except json.JSONDecodeError as e:
|
| 822 |
notify_error(f"JSONパースエラー: {str(e)}")
|
| 823 |
+
return None, {}, gr.update(), gr.update(), gr.update()
|
| 824 |
except Exception as e:
|
| 825 |
notify_error(f"ファイル読み込みエラー: {str(e)}")
|
| 826 |
+
return None, {}, gr.update(), gr.update(), gr.update()
|
| 827 |
|
| 828 |
def convert_people_to_bodies_format(person_data, resolution):
|
| 829 |
"""people形式からbodies形��に変換(表示互換性用)"""
|
|
|
|
| 882 |
input_image.change(
|
| 883 |
fn=on_image_upload,
|
| 884 |
inputs=[input_image],
|
| 885 |
+
outputs=[output_json, pose_data, js_executor, canvas_width, canvas_height]
|
| 886 |
)
|
| 887 |
|
| 888 |
# pose_data変更時にCanvas更新(重要!)- 無限ループ防止
|
|
|
|
| 917 |
template_update_btn.click(
|
| 918 |
fn=load_template_pose,
|
| 919 |
inputs=[template_dropdown],
|
| 920 |
+
outputs=[output_json, pose_data, js_executor, canvas_width, canvas_height]
|
| 921 |
)
|
| 922 |
|
| 923 |
# エクスポートイベント (refs互換DownloadButton方式)
|
|
|
|
| 944 |
json_upload.change(
|
| 945 |
fn=on_json_upload,
|
| 946 |
inputs=[json_upload],
|
| 947 |
+
outputs=[output_json, pose_data, js_executor, canvas_width, canvas_height]
|
| 948 |
+
)
|
| 949 |
+
|
| 950 |
+
# 手動解像度更新ボタン
|
| 951 |
+
canvas_update_btn.click(
|
| 952 |
+
fn=on_canvas_size_update,
|
| 953 |
+
inputs=[canvas_width, canvas_height],
|
| 954 |
+
outputs=[js_executor]
|
| 955 |
)
|
| 956 |
|
| 957 |
return demo
|
| 958 |
|
| 959 |
if __name__ == "__main__":
|
| 960 |
demo = main()
|
| 961 |
+
demo.launch()
|
static/pose_editor.js
CHANGED
|
@@ -15,7 +15,7 @@ window.poseEditorGlobals = {
|
|
| 15 |
isUpdating: false,
|
| 16 |
// 🔧 表示・編集設定
|
| 17 |
enableHands: true,
|
| 18 |
-
enableFace:
|
| 19 |
editMode: "簡易モード", // "簡易モード" or "詳細モード"
|
| 20 |
// 🎨 背景画像機能
|
| 21 |
backgroundImage: null, // 背景画像オブジェクト
|
|
@@ -271,9 +271,12 @@ function updateDisplaySettingsFromCheckbox() {
|
|
| 271 |
// マウス座標取得(refs互換)
|
| 272 |
function getMousePos(event) {
|
| 273 |
const rect = canvas.getBoundingClientRect();
|
|
|
|
|
|
|
|
|
|
| 274 |
return {
|
| 275 |
-
x: event.clientX - rect.left,
|
| 276 |
-
y: event.clientY - rect.top
|
| 277 |
};
|
| 278 |
}
|
| 279 |
|
|
@@ -302,8 +305,7 @@ function findNearestKeypoint(mouseX, mouseY, maxDistance = 20) {
|
|
| 302 |
|
| 303 |
// 📐 解像度情報の取得
|
| 304 |
const originalRes = currentPoseData.resolution || [512, 512];
|
| 305 |
-
const
|
| 306 |
-
const scaleY = canvas.height / originalRes[1];
|
| 307 |
|
| 308 |
for (let i = 0; i < Math.min(20, candidates.length); i++) { // つま先込み20個
|
| 309 |
const point = candidates[i];
|
|
@@ -311,9 +313,9 @@ function findNearestKeypoint(mouseX, mouseY, maxDistance = 20) {
|
|
| 311 |
if (point && point[0] > 1 && point[1] > 1 &&
|
| 312 |
point[0] < originalRes[0] && point[1] < originalRes[1]) {
|
| 313 |
|
| 314 |
-
//
|
| 315 |
-
const scaledX = point[0] *
|
| 316 |
-
const scaledY = point[1] *
|
| 317 |
|
| 318 |
const distance = Math.sqrt((mouseX - scaledX) ** 2 + (mouseY - scaledY) ** 2);
|
| 319 |
|
|
@@ -336,8 +338,7 @@ function findNearestKeypointInDetailMode(mouseX, mouseY, maxDistance = 15) {
|
|
| 336 |
}
|
| 337 |
|
| 338 |
const originalRes = currentPoseData.resolution || [512, 512];
|
| 339 |
-
const
|
| 340 |
-
const scaleY = canvas.height / originalRes[1];
|
| 341 |
|
| 342 |
let nearestKeypoint = null;
|
| 343 |
let minDistance = maxDistance;
|
|
@@ -355,8 +356,8 @@ function findNearestKeypointInDetailMode(mouseX, mouseY, maxDistance = 15) {
|
|
| 355 |
if (handData && handData.length > 0) {
|
| 356 |
for (let i = 0; i < handData.length; i += 3) {
|
| 357 |
if (i + 2 < handData.length) {
|
| 358 |
-
const x = handData[i] *
|
| 359 |
-
const y = handData[i + 1] *
|
| 360 |
const conf = handData[i + 2];
|
| 361 |
|
| 362 |
if (conf > 0.3) {
|
|
@@ -388,8 +389,8 @@ function findNearestKeypointInDetailMode(mouseX, mouseY, maxDistance = 15) {
|
|
| 388 |
const faceData = facesData[0];
|
| 389 |
for (let i = 0; i < faceData.length; i += 3) {
|
| 390 |
if (i + 2 < faceData.length) {
|
| 391 |
-
const x = faceData[i] *
|
| 392 |
-
const y = faceData[i + 1] *
|
| 393 |
const conf = faceData[i + 2];
|
| 394 |
|
| 395 |
if (conf > 0.3) {
|
|
@@ -414,8 +415,9 @@ function findNearestKeypointInDetailMode(mouseX, mouseY, maxDistance = 15) {
|
|
| 414 |
const candidates = currentPoseData.bodies.candidate;
|
| 415 |
const point = candidates[bodyKeypointIndex];
|
| 416 |
if (point) {
|
| 417 |
-
const
|
| 418 |
-
const
|
|
|
|
| 419 |
const distance = Math.sqrt((mouseX - scaledX) ** 2 + (mouseY - scaledY) ** 2);
|
| 420 |
|
| 421 |
if (distance < minDistance) {
|
|
@@ -449,10 +451,9 @@ function findNearestKeypointInDetailMode(clickX, clickY) {
|
|
| 449 |
|
| 450 |
if (currentPoseData && canvas) {
|
| 451 |
const resolution = currentPoseData.resolution || [512, 512];
|
| 452 |
-
const
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
dataClickY = clickY / scaleY;
|
| 456 |
}
|
| 457 |
|
| 458 |
// 体のキーポイント検索
|
|
@@ -716,13 +717,12 @@ function handleMouseDown(event) {
|
|
| 716 |
if (currentPoseData && currentPoseData.bodies && currentPoseData.bodies.candidate) {
|
| 717 |
const candidates = currentPoseData.bodies.candidate;
|
| 718 |
const originalRes = currentPoseData.resolution || [512, 512];
|
| 719 |
-
const
|
| 720 |
-
const scaleY = canvas.height / originalRes[1];
|
| 721 |
|
| 722 |
const point = candidates[keypointIndex];
|
| 723 |
if (point) {
|
| 724 |
-
const keypointX = point[0] *
|
| 725 |
-
const keypointY = point[1] *
|
| 726 |
|
| 727 |
dragOffset = {
|
| 728 |
x: mousePos.x - keypointX,
|
|
@@ -747,12 +747,10 @@ function updateDetailKeypointPosition(detailKeypoint, canvasX, canvasY) {
|
|
| 747 |
}
|
| 748 |
|
| 749 |
const originalRes = currentPoseData.resolution || [512, 512];
|
| 750 |
-
|
| 751 |
-
const
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
const dataX = Math.max(0, Math.min(originalRes[0], canvasX / scaleX));
|
| 755 |
-
const dataY = Math.max(0, Math.min(originalRes[1], canvasY / scaleY));
|
| 756 |
|
| 757 |
switch (detailKeypoint.type) {
|
| 758 |
case 'leftHand':
|
|
@@ -954,8 +952,8 @@ function transformKeypointsInRect(control, newMouseX, newMouseY) {
|
|
| 954 |
dataResolutionHeight = currentPoseData.resolution[1];
|
| 955 |
}
|
| 956 |
|
| 957 |
-
|
| 958 |
-
const
|
| 959 |
|
| 960 |
// 9. 正規化座標かピクセル座標かを判定
|
| 961 |
let isNormalized = false;
|
|
@@ -982,11 +980,13 @@ function transformKeypointsInRect(control, newMouseX, newMouseY) {
|
|
| 982 |
// データ座標→Canvas座標
|
| 983 |
let canvasX, canvasY;
|
| 984 |
if (isNormalized) {
|
| 985 |
-
|
| 986 |
-
|
|
|
|
|
|
|
| 987 |
} else {
|
| 988 |
-
canvasX = x *
|
| 989 |
-
canvasY = y *
|
| 990 |
}
|
| 991 |
|
| 992 |
// 元矩形内での相対位置を計算
|
|
@@ -999,13 +999,13 @@ function transformKeypointsInRect(control, newMouseX, newMouseY) {
|
|
| 999 |
|
| 1000 |
// Canvas座標→データ座標に戻す
|
| 1001 |
if (isNormalized) {
|
| 1002 |
-
const dataX = newCanvasX /
|
| 1003 |
-
const dataY = newCanvasY /
|
| 1004 |
targetKeypoints[i] = dataX / dataResolutionWidth;
|
| 1005 |
targetKeypoints[i + 1] = dataY / dataResolutionHeight;
|
| 1006 |
} else {
|
| 1007 |
-
targetKeypoints[i] = newCanvasX /
|
| 1008 |
-
targetKeypoints[i + 1] = newCanvasY /
|
| 1009 |
}
|
| 1010 |
}
|
| 1011 |
}
|
|
@@ -1080,8 +1080,7 @@ function transformKeypointsDirectly(rectType, originalRect, newRect) {
|
|
| 1080 |
dataResolutionHeight = currentPoseData.resolution[1];
|
| 1081 |
}
|
| 1082 |
|
| 1083 |
-
const
|
| 1084 |
-
const coordScaleY = canvasHeight / dataResolutionHeight;
|
| 1085 |
|
| 1086 |
// 正規化/ピクセル/Canvas座標を判定(元データ優先で判定)
|
| 1087 |
let isNormalized = false;
|
|
@@ -1121,7 +1120,7 @@ function transformKeypointsDirectly(rectType, originalRect, newRect) {
|
|
| 1121 |
sampleCoord: { x: sampleX, y: sampleY },
|
| 1122 |
originalRect: { x: originalRect.x, y: originalRect.y, width: originalRect.width, height: originalRect.height },
|
| 1123 |
newRect: { x: newRect.x, y: newRect.y, width: newRect.width, height: newRect.height },
|
| 1124 |
-
|
| 1125 |
resolution: { data: dataResolutionWidth + 'x' + dataResolutionHeight, canvas: canvasWidth + 'x' + canvasHeight },
|
| 1126 |
keypointsLength: targetKeypoints.length
|
| 1127 |
});
|
|
@@ -1138,13 +1137,13 @@ function transformKeypointsDirectly(rectType, originalRect, newRect) {
|
|
| 1138 |
let y = originalTargetKeypoints[i + 1];
|
| 1139 |
let cx, cy;
|
| 1140 |
if (isNormalized) {
|
| 1141 |
-
cx = (x * dataResolutionWidth) *
|
| 1142 |
-
cy = (y * dataResolutionHeight) *
|
| 1143 |
} else if (isCanvasUnit) {
|
| 1144 |
cx = x; cy = y;
|
| 1145 |
} else {
|
| 1146 |
-
cx = x *
|
| 1147 |
-
cy = y *
|
| 1148 |
}
|
| 1149 |
minX = Math.min(minX, cx);
|
| 1150 |
minY = Math.min(minY, cy);
|
|
@@ -1175,15 +1174,15 @@ function transformKeypointsDirectly(rectType, originalRect, newRect) {
|
|
| 1175 |
// データ座標→Canvas座標(入力の座標系に応じて)
|
| 1176 |
let canvasX, canvasY;
|
| 1177 |
if (isNormalized) {
|
| 1178 |
-
canvasX = (x * dataResolutionWidth) *
|
| 1179 |
-
canvasY = (y * dataResolutionHeight) *
|
| 1180 |
} else if (isCanvasUnit) {
|
| 1181 |
// 既にCanvas座標(過去のバグで混入している場合)
|
| 1182 |
canvasX = x;
|
| 1183 |
canvasY = y;
|
| 1184 |
} else {
|
| 1185 |
-
canvasX = x *
|
| 1186 |
-
canvasY = y *
|
| 1187 |
}
|
| 1188 |
|
| 1189 |
// 元矩形内での相対位置を計算(参照矩形に対して)
|
|
@@ -1200,15 +1199,15 @@ function transformKeypointsDirectly(rectType, originalRect, newRect) {
|
|
| 1200 |
// Canvas座標→データ座標に戻す(常にデータ座標で保存)
|
| 1201 |
let finalX, finalY;
|
| 1202 |
if (isNormalized) {
|
| 1203 |
-
const dataX = newCanvasX /
|
| 1204 |
-
const dataY = newCanvasY /
|
| 1205 |
finalX = dataX / dataResolutionWidth;
|
| 1206 |
finalY = dataY / dataResolutionHeight;
|
| 1207 |
targetKeypoints[i] = finalX;
|
| 1208 |
targetKeypoints[i + 1] = finalY;
|
| 1209 |
} else {
|
| 1210 |
-
finalX = newCanvasX /
|
| 1211 |
-
finalY = newCanvasY /
|
| 1212 |
targetKeypoints[i] = finalX;
|
| 1213 |
targetKeypoints[i + 1] = finalY;
|
| 1214 |
}
|
|
@@ -1326,8 +1325,7 @@ function moveKeypointsFromOriginal(rectType, totalDeltaX, totalDeltaY) {
|
|
| 1326 |
dataResolutionHeight = currentPoseData.resolution[1];
|
| 1327 |
}
|
| 1328 |
|
| 1329 |
-
const
|
| 1330 |
-
const coordScaleY = canvasHeight / dataResolutionHeight;
|
| 1331 |
|
| 1332 |
// 正規化座標かピクセル座標かを判定
|
| 1333 |
let isNormalized = false;
|
|
@@ -1355,11 +1353,11 @@ function moveKeypointsFromOriginal(rectType, totalDeltaX, totalDeltaY) {
|
|
| 1355 |
// データ座標→Canvas座標
|
| 1356 |
let canvasX, canvasY;
|
| 1357 |
if (isNormalized) {
|
| 1358 |
-
canvasX = (origX * dataResolutionWidth) *
|
| 1359 |
-
canvasY = (origY * dataResolutionHeight) *
|
| 1360 |
} else {
|
| 1361 |
-
canvasX = origX *
|
| 1362 |
-
canvasY = origY *
|
| 1363 |
}
|
| 1364 |
|
| 1365 |
// 移動量を適用
|
|
@@ -1368,13 +1366,13 @@ function moveKeypointsFromOriginal(rectType, totalDeltaX, totalDeltaY) {
|
|
| 1368 |
|
| 1369 |
// Canvas座標→データ座標に戻す
|
| 1370 |
if (isNormalized) {
|
| 1371 |
-
const dataX = newCanvasX /
|
| 1372 |
-
const dataY = newCanvasY /
|
| 1373 |
targetKeypoints[i] = dataX / dataResolutionWidth;
|
| 1374 |
targetKeypoints[i + 1] = dataY / dataResolutionHeight;
|
| 1375 |
} else {
|
| 1376 |
-
targetKeypoints[i] = newCanvasX /
|
| 1377 |
-
targetKeypoints[i + 1] = newCanvasY /
|
| 1378 |
}
|
| 1379 |
}
|
| 1380 |
}
|
|
@@ -1429,12 +1427,12 @@ function moveKeypointsWithRect(rectType, deltaX, deltaY) {
|
|
| 1429 |
}
|
| 1430 |
const canvasWidth = window.poseEditorGlobals.canvas ? window.poseEditorGlobals.canvas.width : 512;
|
| 1431 |
const canvasHeight = window.poseEditorGlobals.canvas ? window.poseEditorGlobals.canvas.height : 512;
|
| 1432 |
-
|
| 1433 |
-
const
|
| 1434 |
|
| 1435 |
// Canvasの移動量→データ座標の移動量へ変換
|
| 1436 |
-
const dataDeltaX = deltaX /
|
| 1437 |
-
const dataDeltaY = deltaY /
|
| 1438 |
|
| 1439 |
// すべてのキーポイントを移動(データ座標系)
|
| 1440 |
for (let i = 0; i < keypoints.length; i += 3) {
|
|
@@ -1609,16 +1607,10 @@ function updateKeypointPosition(keypointIndex, canvasX, canvasY) {
|
|
| 1609 |
|
| 1610 |
// 📐 解像度情報の取得
|
| 1611 |
const originalRes = currentPoseData.resolution || [512, 512];
|
| 1612 |
-
|
| 1613 |
-
const
|
| 1614 |
-
|
| 1615 |
-
|
| 1616 |
-
const dataX = canvasX / scaleX;
|
| 1617 |
-
const dataY = canvasY / scaleY;
|
| 1618 |
-
|
| 1619 |
-
// 🔧 refs互換:データ座標系でクランプ(0〜解像度内)
|
| 1620 |
-
const clampedDataX = Math.max(0, Math.min(originalRes[0], dataX));
|
| 1621 |
-
const clampedDataY = Math.max(0, Math.min(originalRes[1], dataY));
|
| 1622 |
|
| 1623 |
// 1. candidateリストを更新
|
| 1624 |
const candidates = currentPoseData.bodies.candidate;
|
|
@@ -1908,8 +1900,7 @@ function drawPose(poseData, enableHands = true, enableFace = true, highlightInde
|
|
| 1908 |
|
| 1909 |
// 📐 解像度情報の取得(手と顔描画のため)
|
| 1910 |
const originalRes = currentPoseData.resolution || [512, 512];
|
| 1911 |
-
const
|
| 1912 |
-
const scaleY = canvas.height / originalRes[1];
|
| 1913 |
|
| 1914 |
|
| 1915 |
// ボディの描画(ハイライト対応)
|
|
@@ -1944,7 +1935,7 @@ function drawPose(poseData, enableHands = true, enableFace = true, highlightInde
|
|
| 1944 |
|
| 1945 |
if (handsDataForDrawing && handsDataForDrawing.length >= 2) {
|
| 1946 |
if (window.poseEditorDebug.hands) console.log('✅ Calling drawHands function');
|
| 1947 |
-
drawHands(handsDataForDrawing, originalRes
|
| 1948 |
} else {
|
| 1949 |
if (window.poseEditorDebug.hands) console.log('❌ Invalid hands data for drawing');
|
| 1950 |
}
|
|
@@ -1962,10 +1953,10 @@ function drawPose(poseData, enableHands = true, enableFace = true, highlightInde
|
|
| 1962 |
person.hand_left_keypoints_2d || [],
|
| 1963 |
person.hand_right_keypoints_2d || []
|
| 1964 |
];
|
| 1965 |
-
drawHandRectangles(editedHandsData, originalRes
|
| 1966 |
} else {
|
| 1967 |
// 💖 people形式のみサポート、空データで矩形なし
|
| 1968 |
-
drawHandRectangles([[], []], originalRes
|
| 1969 |
}
|
| 1970 |
} else {
|
| 1971 |
// 編集モード中:既存の矩形を描画(再計算しない)
|
|
@@ -1992,7 +1983,7 @@ function drawPose(poseData, enableHands = true, enableFace = true, highlightInde
|
|
| 1992 |
}
|
| 1993 |
|
| 1994 |
if (facesDataForDrawing && facesDataForDrawing.length > 0) {
|
| 1995 |
-
drawFaces(facesDataForDrawing, originalRes
|
| 1996 |
}
|
| 1997 |
} catch (error) {
|
| 1998 |
console.error("❌ Error drawing face:", error);
|
|
@@ -2004,9 +1995,9 @@ function drawPose(poseData, enableHands = true, enableFace = true, highlightInde
|
|
| 2004 |
// 🚀 通常時:編集済みキーポイントから矩形を計算
|
| 2005 |
if (currentPoseData.people && currentPoseData.people[0] && currentPoseData.people[0].face_keypoints_2d) {
|
| 2006 |
const editedFacesData = [currentPoseData.people[0].face_keypoints_2d];
|
| 2007 |
-
drawFaceRectangles(editedFacesData, originalRes
|
| 2008 |
} else {
|
| 2009 |
-
drawFaceRectangles(currentPoseData.faces, originalRes
|
| 2010 |
}
|
| 2011 |
} else {
|
| 2012 |
// 編集モード中:既存の矩形を描画(再計算しない)
|
|
@@ -2030,34 +2021,28 @@ function drawBackground() {
|
|
| 2030 |
ctx.fillStyle = '#ffffff';
|
| 2031 |
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
| 2032 |
|
| 2033 |
-
//
|
| 2034 |
if (window.poseEditorGlobals.backgroundImage) {
|
| 2035 |
const img = window.poseEditorGlobals.backgroundImage;
|
| 2036 |
-
|
| 2037 |
-
// アスペクト比を保持してCanvas内に収める
|
| 2038 |
const imgAspect = img.width / img.height;
|
| 2039 |
const canvasAspect = canvas.width / canvas.height;
|
| 2040 |
-
|
| 2041 |
let drawWidth, drawHeight, offsetX, offsetY;
|
| 2042 |
-
|
| 2043 |
if (imgAspect > canvasAspect) {
|
| 2044 |
-
// 画像の方が横長 → 幅をCanvas
|
| 2045 |
drawWidth = canvas.width;
|
| 2046 |
-
drawHeight = canvas.width / imgAspect;
|
| 2047 |
offsetX = 0;
|
| 2048 |
-
offsetY = (canvas.height - drawHeight) / 2;
|
| 2049 |
} else {
|
| 2050 |
-
// 画像の方が縦長 → 高さをCanvas
|
| 2051 |
drawHeight = canvas.height;
|
| 2052 |
-
drawWidth = canvas.height * imgAspect;
|
| 2053 |
-
offsetX = (canvas.width - drawWidth) / 2;
|
| 2054 |
offsetY = 0;
|
| 2055 |
}
|
| 2056 |
-
|
| 2057 |
-
// 画像を暗くして描画(30%透明度)
|
| 2058 |
ctx.globalAlpha = 0.3;
|
| 2059 |
ctx.drawImage(img, offsetX, offsetY, drawWidth, drawHeight);
|
| 2060 |
-
ctx.globalAlpha = 1.0;
|
| 2061 |
}
|
| 2062 |
}
|
| 2063 |
|
|
@@ -2097,8 +2082,7 @@ function drawBody(poseData, highlightIndex = -1) {
|
|
| 2097 |
|
| 2098 |
// 📐 解像度情報の取得
|
| 2099 |
const originalRes = poseData.resolution || [512, 512];
|
| 2100 |
-
const
|
| 2101 |
-
const scaleY = canvas.height / originalRes[1];
|
| 2102 |
|
| 2103 |
// 接続線の描画(refs互換・配列ベース + 座標変換)
|
| 2104 |
ctx.lineWidth = 3;
|
|
@@ -2118,11 +2102,11 @@ function drawBody(poseData, highlightIndex = -1) {
|
|
| 2118 |
startPoint[0] < originalRes[0] && startPoint[1] < originalRes[1] &&
|
| 2119 |
endPoint[0] < originalRes[0] && endPoint[1] < originalRes[1]) {
|
| 2120 |
|
| 2121 |
-
// 🔄
|
| 2122 |
-
const startX = startPoint[0] *
|
| 2123 |
-
const startY = startPoint[1] *
|
| 2124 |
-
const endX = endPoint[0] *
|
| 2125 |
-
const endY = endPoint[1] *
|
| 2126 |
|
| 2127 |
// 🔧 refs互換: SKELETON_COLORSの配列ベース色分け
|
| 2128 |
ctx.strokeStyle = SKELETON_COLORS[i % SKELETON_COLORS.length];
|
|
@@ -2148,9 +2132,9 @@ function drawBody(poseData, highlightIndex = -1) {
|
|
| 2148 |
if (point && point[0] > 1 && point[1] > 1 &&
|
| 2149 |
point[0] < originalRes[0] && point[1] < originalRes[1]) {
|
| 2150 |
|
| 2151 |
-
// 🔄
|
| 2152 |
-
const scaledX = point[0] *
|
| 2153 |
-
const scaledY = point[1] *
|
| 2154 |
|
| 2155 |
// 🔧 ハイライト対応: ドラッグ中のキーポイントを強調表示
|
| 2156 |
if (i === highlightIndex) {
|
|
@@ -2175,8 +2159,8 @@ function drawBody(poseData, highlightIndex = -1) {
|
|
| 2175 |
|
| 2176 |
// 🎨 補間機能: 有効キーポイントが少ない場合の視覚的改善
|
| 2177 |
if (drawnKeypoints < 10) {
|
| 2178 |
-
|
| 2179 |
-
|
| 2180 |
}
|
| 2181 |
}
|
| 2182 |
|
|
@@ -2189,7 +2173,7 @@ function drawKeypoint(x, y, radius = KEYPOINT_RADIUS) {
|
|
| 2189 |
}
|
| 2190 |
|
| 2191 |
// 手の描画(21キーポイント × 2)- refs互換
|
| 2192 |
-
function drawHands(handsData, originalRes,
|
| 2193 |
if (!handsData || handsData.length === 0) return;
|
| 2194 |
|
| 2195 |
// console.log(`[DEBUG] 👋 Drawing hands with ${handsData.length} hand(s) - refs互換`);
|
|
@@ -2219,9 +2203,10 @@ function drawHands(handsData, originalRes, scaleX, scaleY) {
|
|
| 2219 |
const conf = hand[i + 2];
|
| 2220 |
|
| 2221 |
if (conf > 0.1) { // refs互換の閾値
|
| 2222 |
-
//
|
| 2223 |
-
const
|
| 2224 |
-
const
|
|
|
|
| 2225 |
handKeypoints.push([scaledX, scaledY, conf]);
|
| 2226 |
} else {
|
| 2227 |
handKeypoints.push([0, 0, 0]); // 無効キーポイント
|
|
@@ -2282,7 +2267,7 @@ function drawHands(handsData, originalRes, scaleX, scaleY) {
|
|
| 2282 |
}
|
| 2283 |
|
| 2284 |
// 顔の描画(68キーポイント)- refs互換
|
| 2285 |
-
function drawFaces(facesData, originalRes,
|
| 2286 |
if (!facesData || facesData.length === 0) return;
|
| 2287 |
|
| 2288 |
// console.log(`[DEBUG] 👤 Drawing faces with ${facesData.length} face(s) - refs互換`);
|
|
@@ -2297,9 +2282,10 @@ function drawFaces(facesData, originalRes, scaleX, scaleY) {
|
|
| 2297 |
const conf = face[i + 2];
|
| 2298 |
|
| 2299 |
if (conf > 0.1) { // refs互換の閾値
|
| 2300 |
-
//
|
| 2301 |
-
const
|
| 2302 |
-
const
|
|
|
|
| 2303 |
faceKeypoints.push([scaledX, scaledY, conf]);
|
| 2304 |
} else {
|
| 2305 |
faceKeypoints.push([0, 0, 0]); // 無効キーポイント
|
|
@@ -2366,6 +2352,40 @@ let coordinateTransformer = {
|
|
| 2366 |
}
|
| 2367 |
};
|
| 2368 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2369 |
// データ解像度とCanvas表示サイズの変換(後方互換性)
|
| 2370 |
function transformCoordinate(x, y, dataWidth, dataHeight) {
|
| 2371 |
const scaleX = canvas.width / dataWidth;
|
|
@@ -2379,25 +2399,229 @@ function transformCoordinate(x, y, dataWidth, dataHeight) {
|
|
| 2379 |
|
| 2380 |
// 描画時に座標変換を適用
|
| 2381 |
function drawKeypointScaled(x, y, dataRes, radius = KEYPOINT_RADIUS) {
|
| 2382 |
-
const scaled =
|
| 2383 |
drawKeypoint(scaled.x, scaled.y, radius);
|
| 2384 |
}
|
| 2385 |
|
| 2386 |
// Canvas解像度更新
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2387 |
function updateCanvasResolution(width, height) {
|
| 2388 |
if (!canvas) return false;
|
| 2389 |
-
|
| 2390 |
-
|
| 2391 |
-
|
| 2392 |
-
|
| 2393 |
-
|
| 2394 |
-
|
| 2395 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2396 |
if (poseData) {
|
| 2397 |
-
drawPose(poseData);
|
|
|
|
|
|
|
| 2398 |
}
|
| 2399 |
-
|
| 2400 |
-
notifyCanvasOperation(`Canvas
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2401 |
return true;
|
| 2402 |
}
|
| 2403 |
|
|
@@ -2480,6 +2704,29 @@ window.gradioCanvasUpdate = function(pose_json_str) {
|
|
| 2480 |
window.poseEditorGlobals.poseData = poseData;
|
| 2481 |
}
|
| 2482 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2483 |
// 💖 originalKeypointsも設定(但し、baseOriginalKeypointsは保護)
|
| 2484 |
if (poseData && poseData.people && poseData.people[0]) {
|
| 2485 |
// baseOriginalKeypointsは上書きしない(編集セッション保持のため)
|
|
@@ -2529,6 +2776,10 @@ window.gradioCanvasUpdate = function(pose_json_str) {
|
|
| 2529 |
|
| 2530 |
// グローバル設定で描画(手・顔表示設定を反映)
|
| 2531 |
if (poseData && Object.keys(poseData).length > 0) {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2532 |
drawPose(
|
| 2533 |
poseData,
|
| 2534 |
currentHandsEnabled,
|
|
@@ -2705,7 +2956,7 @@ function safeCanvasOperation(operation) {
|
|
| 2705 |
|
| 2706 |
|
| 2707 |
// 🔧 簡易モード:手の矩形描画(refs互換)
|
| 2708 |
-
function drawHandRectangles(handsData, originalRes,
|
| 2709 |
if (!handsData || handsData.length === 0) return;
|
| 2710 |
|
| 2711 |
|
|
@@ -2717,7 +2968,7 @@ function drawHandRectangles(handsData, originalRes, scaleX, scaleY) {
|
|
| 2717 |
|
| 2718 |
handsData.forEach((hand, handIndex) => {
|
| 2719 |
if (hand && hand.length > 0) {
|
| 2720 |
-
const rect = calculateHandRect(hand, originalRes
|
| 2721 |
if (rect) {
|
| 2722 |
const handType = HAND_TYPES[handIndex] || `hand_${handIndex}`;
|
| 2723 |
|
|
@@ -2736,7 +2987,7 @@ function drawHandRectangles(handsData, originalRes, scaleX, scaleY) {
|
|
| 2736 |
}
|
| 2737 |
|
| 2738 |
// 🔧 簡易モード:顔の矩形描画(refs互換)
|
| 2739 |
-
function drawFaceRectangles(facesData, originalRes,
|
| 2740 |
if (!facesData || facesData.length === 0) return;
|
| 2741 |
|
| 2742 |
|
|
@@ -2747,7 +2998,7 @@ function drawFaceRectangles(facesData, originalRes, scaleX, scaleY) {
|
|
| 2747 |
|
| 2748 |
const face = facesData[0]; // 最初の顔のみ(編集済みデータ)
|
| 2749 |
if (face && face.length > 0) {
|
| 2750 |
-
const rect = calculateFaceRect(face, originalRes
|
| 2751 |
if (rect) {
|
| 2752 |
// 🔧 グローバルに矩形情報保存(編集モード中は既存の矩形を保持)
|
| 2753 |
if (!window.poseEditorGlobals.currentRects.face || !window.poseEditorGlobals.rectEditModeActive) {
|
|
@@ -2774,11 +3025,10 @@ function calculateHandRect(handData, originalRes, scaleX, scaleY) {
|
|
| 2774 |
const confidence = handData[i + 2];
|
| 2775 |
|
| 2776 |
if (confidence > 0.3) { // refs互換の閾値
|
| 2777 |
-
// 🔧
|
| 2778 |
-
|
| 2779 |
-
|
| 2780 |
-
const
|
| 2781 |
-
const finalY = y * scaleY;
|
| 2782 |
|
| 2783 |
minX = Math.min(minX, finalX);
|
| 2784 |
minY = Math.min(minY, finalY);
|
|
@@ -2861,8 +3111,9 @@ function calculateFaceRect(faceData, originalRes, scaleX, scaleY) {
|
|
| 2861 |
const confidence = faceData[i + 2];
|
| 2862 |
|
| 2863 |
if (confidence > 0.3) { // refs互換の閾値
|
| 2864 |
-
const
|
| 2865 |
-
const
|
|
|
|
| 2866 |
|
| 2867 |
minX = Math.min(minX, scaledX);
|
| 2868 |
minY = Math.min(minY, scaledY);
|
|
@@ -2960,8 +3211,7 @@ function redrawPoseWithoutRecalculation() {
|
|
| 2960 |
|
| 2961 |
// 📐 解像度情報の取得
|
| 2962 |
const originalRes = currentPoseData.resolution || [512, 512];
|
| 2963 |
-
const
|
| 2964 |
-
const scaleY = canvas.height / originalRes[1];
|
| 2965 |
|
| 2966 |
// ボディの描画(ハイライトなし)
|
| 2967 |
drawBody(currentPoseData, -1);
|
|
@@ -2974,12 +3224,12 @@ function redrawPoseWithoutRecalculation() {
|
|
| 2974 |
person.hand_left_keypoints_2d || [],
|
| 2975 |
person.hand_right_keypoints_2d || []
|
| 2976 |
];
|
| 2977 |
-
drawHands(handsData, originalRes
|
| 2978 |
}
|
| 2979 |
|
| 2980 |
// 顔の描画(設定制御・座標変換パラメータ付き)
|
| 2981 |
if (window.poseEditorGlobals.enableFace && currentPoseData.faces) {
|
| 2982 |
-
drawFaces(currentPoseData.faces, originalRes
|
| 2983 |
}
|
| 2984 |
|
| 2985 |
// 🔧 既存の矩形のみ描画(再計算なし)
|
|
@@ -3350,18 +3600,17 @@ function updateKeypointsArrayByRect(keypointsArray, originalKeypointsArray, orig
|
|
| 3350 |
const origY = originalKeypointsArray[i + 1];
|
| 3351 |
|
| 3352 |
if (origX > 0 && origY > 0) {
|
| 3353 |
-
// 🚀
|
| 3354 |
-
const
|
| 3355 |
-
const
|
| 3356 |
-
|
| 3357 |
-
const
|
| 3358 |
-
const
|
| 3359 |
-
const
|
| 3360 |
-
const
|
| 3361 |
-
const
|
| 3362 |
-
const
|
| 3363 |
-
const
|
| 3364 |
-
const newRectHeightData = newRect.height / scaleY;
|
| 3365 |
|
| 3366 |
// 元矩形内の相対位置を計算
|
| 3367 |
const relativeX = (origX - origRectDataX) / origRectWidthData;
|
|
@@ -3410,15 +3659,17 @@ function moveKeypointsArray(keypointsArray, deltaX, deltaY, label) {
|
|
| 3410 |
const currentX = keypointsArray[i];
|
| 3411 |
const currentY = keypointsArray[i + 1];
|
| 3412 |
|
| 3413 |
-
// 🚀
|
| 3414 |
-
const
|
| 3415 |
-
const
|
| 3416 |
-
const dataDeltaX = deltaX /
|
| 3417 |
-
const dataDeltaY = deltaY /
|
| 3418 |
|
| 3419 |
// 移動(512x512にクランプ)
|
| 3420 |
-
const
|
| 3421 |
-
const
|
|
|
|
|
|
|
| 3422 |
|
| 3423 |
keypointsArray[i] = newX;
|
| 3424 |
keypointsArray[i + 1] = newY;
|
|
@@ -3558,8 +3809,7 @@ function updateKeypointsByRect(rectType, newRect) {
|
|
| 3558 |
dataResolutionHeight = currentPoseData.resolution[1];
|
| 3559 |
}
|
| 3560 |
|
| 3561 |
-
const
|
| 3562 |
-
const coordScaleY = canvas.height / dataResolutionHeight;
|
| 3563 |
|
| 3564 |
// 正規化座標かピクセル座標かを判定(refs互換)
|
| 3565 |
let isNormalized = false;
|
|
@@ -3588,11 +3838,11 @@ function updateKeypointsByRect(rectType, newRect) {
|
|
| 3588 |
// データ座標→Canvas座標
|
| 3589 |
let canvasX, canvasY;
|
| 3590 |
if (isNormalized) {
|
| 3591 |
-
canvasX = (x * dataResolutionWidth) *
|
| 3592 |
-
canvasY = (y * dataResolutionHeight) *
|
| 3593 |
} else {
|
| 3594 |
-
canvasX = x *
|
| 3595 |
-
canvasY = y *
|
| 3596 |
}
|
| 3597 |
|
| 3598 |
// 🔧 元矩形内での相対位置を計算(refs互換・安全範囲チェック)
|
|
@@ -3609,8 +3859,8 @@ function updateKeypointsByRect(rectType, newRect) {
|
|
| 3609 |
|
| 3610 |
// 🔧 Canvas座標→データ座標に戻す(refs互換・範囲制限付き)
|
| 3611 |
if (isNormalized) {
|
| 3612 |
-
const dataX = newCanvasX /
|
| 3613 |
-
const dataY = newCanvasY /
|
| 3614 |
let newNormX = dataX / dataResolutionWidth;
|
| 3615 |
let newNormY = dataY / dataResolutionHeight;
|
| 3616 |
|
|
@@ -3621,8 +3871,8 @@ function updateKeypointsByRect(rectType, newRect) {
|
|
| 3621 |
targetKeypoints[i] = newNormX;
|
| 3622 |
targetKeypoints[i + 1] = newNormY;
|
| 3623 |
} else {
|
| 3624 |
-
let newDataX = newCanvasX /
|
| 3625 |
-
let newDataY = newCanvasY /
|
| 3626 |
|
| 3627 |
// ピクセル座標の範囲制限
|
| 3628 |
newDataX = Math.max(0, Math.min(dataResolutionWidth, newDataX));
|
|
@@ -3706,8 +3956,7 @@ function moveKeypointsByRect(rectType, deltaX, deltaY) {
|
|
| 3706 |
dataResolutionHeight = currentPoseData.resolution[1];
|
| 3707 |
}
|
| 3708 |
|
| 3709 |
-
const
|
| 3710 |
-
const coordScaleY = canvas.height / dataResolutionHeight;
|
| 3711 |
|
| 3712 |
// 正規化座標かピクセル座標かを判定(refs互換)
|
| 3713 |
let isNormalized = false;
|
|
@@ -3723,8 +3972,8 @@ function moveKeypointsByRect(rectType, deltaX, deltaY) {
|
|
| 3723 |
}
|
| 3724 |
|
| 3725 |
// Canvas座標での移動量をデータ座標での移動量に変換
|
| 3726 |
-
const dataDeltaX = deltaX /
|
| 3727 |
-
const dataDeltaY = deltaY /
|
| 3728 |
|
| 3729 |
|
| 3730 |
let movedCount = 0;
|
|
@@ -3751,8 +4000,9 @@ function moveKeypointsByRect(rectType, deltaX, deltaY) {
|
|
| 3751 |
}
|
| 3752 |
|
| 3753 |
// 🎨 推定接続の描画(少ないキーポイント用の補間機能)
|
| 3754 |
-
function drawEstimatedConnections(candidates, originalRes
|
| 3755 |
const ctx = window.poseEditorGlobals.ctx;
|
|
|
|
| 3756 |
|
| 3757 |
// 有効なキーポイントを取得
|
| 3758 |
const validPoints = [];
|
|
@@ -3762,8 +4012,8 @@ function drawEstimatedConnections(candidates, originalRes, scaleX, scaleY) {
|
|
| 3762 |
point[0] < originalRes[0] && point[1] < originalRes[1]) {
|
| 3763 |
validPoints.push({
|
| 3764 |
index: i,
|
| 3765 |
-
x: point[0] *
|
| 3766 |
-
y: point[1] *
|
| 3767 |
originalX: point[0],
|
| 3768 |
originalY: point[1]
|
| 3769 |
});
|
|
@@ -3807,8 +4057,72 @@ function drawEstimatedConnections(candidates, originalRes, scaleX, scaleY) {
|
|
| 3807 |
}
|
| 3808 |
|
| 3809 |
// スタイルをリセット
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3810 |
ctx.setLineDash([]); // 実線に戻す
|
| 3811 |
ctx.globalAlpha = 1.0; // 不透明に戻す
|
| 3812 |
}
|
| 3813 |
|
| 3814 |
// 🎨 pose_editor.js initialization complete
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
isUpdating: false,
|
| 16 |
// 🔧 表示・編集設定
|
| 17 |
enableHands: true,
|
| 18 |
+
enableFace: false,
|
| 19 |
editMode: "簡易モード", // "簡易モード" or "詳細モード"
|
| 20 |
// 🎨 背景画像機能
|
| 21 |
backgroundImage: null, // 背景画像オブジェクト
|
|
|
|
| 271 |
// マウス座標取得(refs互換)
|
| 272 |
function getMousePos(event) {
|
| 273 |
const rect = canvas.getBoundingClientRect();
|
| 274 |
+
// CSSピクセル → Canvas内部ピクセルへ正規化(非スクエア時のズレ解消)
|
| 275 |
+
const scaleX = canvas.width / rect.width;
|
| 276 |
+
const scaleY = canvas.height / rect.height;
|
| 277 |
return {
|
| 278 |
+
x: (event.clientX - rect.left) * scaleX,
|
| 279 |
+
y: (event.clientY - rect.top) * scaleY
|
| 280 |
};
|
| 281 |
}
|
| 282 |
|
|
|
|
| 305 |
|
| 306 |
// 📐 解像度情報の取得
|
| 307 |
const originalRes = currentPoseData.resolution || [512, 512];
|
| 308 |
+
const fit = getFitParams(originalRes);
|
|
|
|
| 309 |
|
| 310 |
for (let i = 0; i < Math.min(20, candidates.length); i++) { // つま先込み20個
|
| 311 |
const point = candidates[i];
|
|
|
|
| 313 |
if (point && point[0] > 1 && point[1] > 1 &&
|
| 314 |
point[0] < originalRes[0] && point[1] < originalRes[1]) {
|
| 315 |
|
| 316 |
+
// 座標変換を適用(アスペクト比維持 + オフセット)
|
| 317 |
+
const scaledX = fit.offsetX + point[0] * fit.scale;
|
| 318 |
+
const scaledY = fit.offsetY + point[1] * fit.scale;
|
| 319 |
|
| 320 |
const distance = Math.sqrt((mouseX - scaledX) ** 2 + (mouseY - scaledY) ** 2);
|
| 321 |
|
|
|
|
| 338 |
}
|
| 339 |
|
| 340 |
const originalRes = currentPoseData.resolution || [512, 512];
|
| 341 |
+
const fit = getFitParams(originalRes);
|
|
|
|
| 342 |
|
| 343 |
let nearestKeypoint = null;
|
| 344 |
let minDistance = maxDistance;
|
|
|
|
| 356 |
if (handData && handData.length > 0) {
|
| 357 |
for (let i = 0; i < handData.length; i += 3) {
|
| 358 |
if (i + 2 < handData.length) {
|
| 359 |
+
const x = fit.offsetX + handData[i] * fit.scale;
|
| 360 |
+
const y = fit.offsetY + handData[i + 1] * fit.scale;
|
| 361 |
const conf = handData[i + 2];
|
| 362 |
|
| 363 |
if (conf > 0.3) {
|
|
|
|
| 389 |
const faceData = facesData[0];
|
| 390 |
for (let i = 0; i < faceData.length; i += 3) {
|
| 391 |
if (i + 2 < faceData.length) {
|
| 392 |
+
const x = fit.offsetX + faceData[i] * fit.scale;
|
| 393 |
+
const y = fit.offsetY + faceData[i + 1] * fit.scale;
|
| 394 |
const conf = faceData[i + 2];
|
| 395 |
|
| 396 |
if (conf > 0.3) {
|
|
|
|
| 415 |
const candidates = currentPoseData.bodies.candidate;
|
| 416 |
const point = candidates[bodyKeypointIndex];
|
| 417 |
if (point) {
|
| 418 |
+
const fit = getFitParams(currentPoseData.resolution || [512,512]);
|
| 419 |
+
const scaledX = fit.offsetX + point[0] * fit.scale;
|
| 420 |
+
const scaledY = fit.offsetY + point[1] * fit.scale;
|
| 421 |
const distance = Math.sqrt((mouseX - scaledX) ** 2 + (mouseY - scaledY) ** 2);
|
| 422 |
|
| 423 |
if (distance < minDistance) {
|
|
|
|
| 451 |
|
| 452 |
if (currentPoseData && canvas) {
|
| 453 |
const resolution = currentPoseData.resolution || [512, 512];
|
| 454 |
+
const d = canvasToDataXY(clickX, clickY, resolution);
|
| 455 |
+
dataClickX = d.x;
|
| 456 |
+
dataClickY = d.y;
|
|
|
|
| 457 |
}
|
| 458 |
|
| 459 |
// 体のキーポイント検索
|
|
|
|
| 717 |
if (currentPoseData && currentPoseData.bodies && currentPoseData.bodies.candidate) {
|
| 718 |
const candidates = currentPoseData.bodies.candidate;
|
| 719 |
const originalRes = currentPoseData.resolution || [512, 512];
|
| 720 |
+
const fit = getFitParams(originalRes);
|
|
|
|
| 721 |
|
| 722 |
const point = candidates[keypointIndex];
|
| 723 |
if (point) {
|
| 724 |
+
const keypointX = fit.offsetX + point[0] * fit.scale;
|
| 725 |
+
const keypointY = fit.offsetY + point[1] * fit.scale;
|
| 726 |
|
| 727 |
dragOffset = {
|
| 728 |
x: mousePos.x - keypointX,
|
|
|
|
| 747 |
}
|
| 748 |
|
| 749 |
const originalRes = currentPoseData.resolution || [512, 512];
|
| 750 |
+
// Canvas座標をデータ座標に変換(レターボックス対応)
|
| 751 |
+
const dataPt = canvasToDataXY(canvasX, canvasY, originalRes);
|
| 752 |
+
const dataX = dataPt.x;
|
| 753 |
+
const dataY = dataPt.y;
|
|
|
|
|
|
|
| 754 |
|
| 755 |
switch (detailKeypoint.type) {
|
| 756 |
case 'leftHand':
|
|
|
|
| 952 |
dataResolutionHeight = currentPoseData.resolution[1];
|
| 953 |
}
|
| 954 |
|
| 955 |
+
// レターボックス対応フィット
|
| 956 |
+
const fit = getFitParams([dataResolutionWidth, dataResolutionHeight]);
|
| 957 |
|
| 958 |
// 9. 正規化座標かピクセル座標かを判定
|
| 959 |
let isNormalized = false;
|
|
|
|
| 980 |
// データ座標→Canvas座標
|
| 981 |
let canvasX, canvasY;
|
| 982 |
if (isNormalized) {
|
| 983 |
+
const dx = x * dataResolutionWidth;
|
| 984 |
+
const dy = y * dataResolutionHeight;
|
| 985 |
+
canvasX = fit.offsetX + dx * fit.scale;
|
| 986 |
+
canvasY = fit.offsetY + dy * fit.scale;
|
| 987 |
} else {
|
| 988 |
+
canvasX = fit.offsetX + x * fit.scale;
|
| 989 |
+
canvasY = fit.offsetY + y * fit.scale;
|
| 990 |
}
|
| 991 |
|
| 992 |
// 元矩形内での相対位置を計算
|
|
|
|
| 999 |
|
| 1000 |
// Canvas座標→データ座標に戻す
|
| 1001 |
if (isNormalized) {
|
| 1002 |
+
const dataX = (newCanvasX - fit.offsetX) / fit.scale;
|
| 1003 |
+
const dataY = (newCanvasY - fit.offsetY) / fit.scale;
|
| 1004 |
targetKeypoints[i] = dataX / dataResolutionWidth;
|
| 1005 |
targetKeypoints[i + 1] = dataY / dataResolutionHeight;
|
| 1006 |
} else {
|
| 1007 |
+
targetKeypoints[i] = (newCanvasX - fit.offsetX) / fit.scale;
|
| 1008 |
+
targetKeypoints[i + 1] = (newCanvasY - fit.offsetY) / fit.scale;
|
| 1009 |
}
|
| 1010 |
}
|
| 1011 |
}
|
|
|
|
| 1080 |
dataResolutionHeight = currentPoseData.resolution[1];
|
| 1081 |
}
|
| 1082 |
|
| 1083 |
+
const fit = getFitParams([dataResolutionWidth, dataResolutionHeight]);
|
|
|
|
| 1084 |
|
| 1085 |
// 正規化/ピクセル/Canvas座標を判定(元データ優先で判定)
|
| 1086 |
let isNormalized = false;
|
|
|
|
| 1120 |
sampleCoord: { x: sampleX, y: sampleY },
|
| 1121 |
originalRect: { x: originalRect.x, y: originalRect.y, width: originalRect.width, height: originalRect.height },
|
| 1122 |
newRect: { x: newRect.x, y: newRect.y, width: newRect.width, height: newRect.height },
|
| 1123 |
+
fit: { scale: fit.scale, offsetX: fit.offsetX, offsetY: fit.offsetY },
|
| 1124 |
resolution: { data: dataResolutionWidth + 'x' + dataResolutionHeight, canvas: canvasWidth + 'x' + canvasHeight },
|
| 1125 |
keypointsLength: targetKeypoints.length
|
| 1126 |
});
|
|
|
|
| 1137 |
let y = originalTargetKeypoints[i + 1];
|
| 1138 |
let cx, cy;
|
| 1139 |
if (isNormalized) {
|
| 1140 |
+
cx = fit.offsetX + (x * dataResolutionWidth) * fit.scale;
|
| 1141 |
+
cy = fit.offsetY + (y * dataResolutionHeight) * fit.scale;
|
| 1142 |
} else if (isCanvasUnit) {
|
| 1143 |
cx = x; cy = y;
|
| 1144 |
} else {
|
| 1145 |
+
cx = fit.offsetX + x * fit.scale;
|
| 1146 |
+
cy = fit.offsetY + y * fit.scale;
|
| 1147 |
}
|
| 1148 |
minX = Math.min(minX, cx);
|
| 1149 |
minY = Math.min(minY, cy);
|
|
|
|
| 1174 |
// データ座標→Canvas座標(入力の座標系に応じて)
|
| 1175 |
let canvasX, canvasY;
|
| 1176 |
if (isNormalized) {
|
| 1177 |
+
canvasX = fit.offsetX + (x * dataResolutionWidth) * fit.scale;
|
| 1178 |
+
canvasY = fit.offsetY + (y * dataResolutionHeight) * fit.scale;
|
| 1179 |
} else if (isCanvasUnit) {
|
| 1180 |
// 既にCanvas座標(過去のバグで混入している場合)
|
| 1181 |
canvasX = x;
|
| 1182 |
canvasY = y;
|
| 1183 |
} else {
|
| 1184 |
+
canvasX = fit.offsetX + x * fit.scale;
|
| 1185 |
+
canvasY = fit.offsetY + y * fit.scale;
|
| 1186 |
}
|
| 1187 |
|
| 1188 |
// 元矩形内での相対位置を計算(参照矩形に対して)
|
|
|
|
| 1199 |
// Canvas座標→データ座標に戻す(常にデータ座標で保存)
|
| 1200 |
let finalX, finalY;
|
| 1201 |
if (isNormalized) {
|
| 1202 |
+
const dataX = (newCanvasX - fit.offsetX) / fit.scale;
|
| 1203 |
+
const dataY = (newCanvasY - fit.offsetY) / fit.scale;
|
| 1204 |
finalX = dataX / dataResolutionWidth;
|
| 1205 |
finalY = dataY / dataResolutionHeight;
|
| 1206 |
targetKeypoints[i] = finalX;
|
| 1207 |
targetKeypoints[i + 1] = finalY;
|
| 1208 |
} else {
|
| 1209 |
+
finalX = (newCanvasX - fit.offsetX) / fit.scale;
|
| 1210 |
+
finalY = (newCanvasY - fit.offsetY) / fit.scale;
|
| 1211 |
targetKeypoints[i] = finalX;
|
| 1212 |
targetKeypoints[i + 1] = finalY;
|
| 1213 |
}
|
|
|
|
| 1325 |
dataResolutionHeight = currentPoseData.resolution[1];
|
| 1326 |
}
|
| 1327 |
|
| 1328 |
+
const fit = getFitParams([dataResolutionWidth, dataResolutionHeight]);
|
|
|
|
| 1329 |
|
| 1330 |
// 正規化座標かピクセル座標かを判定
|
| 1331 |
let isNormalized = false;
|
|
|
|
| 1353 |
// データ座標→Canvas座標
|
| 1354 |
let canvasX, canvasY;
|
| 1355 |
if (isNormalized) {
|
| 1356 |
+
canvasX = fit.offsetX + (origX * dataResolutionWidth) * fit.scale;
|
| 1357 |
+
canvasY = fit.offsetY + (origY * dataResolutionHeight) * fit.scale;
|
| 1358 |
} else {
|
| 1359 |
+
canvasX = fit.offsetX + origX * fit.scale;
|
| 1360 |
+
canvasY = fit.offsetY + origY * fit.scale;
|
| 1361 |
}
|
| 1362 |
|
| 1363 |
// 移動量を適用
|
|
|
|
| 1366 |
|
| 1367 |
// Canvas座標→データ座標に戻す
|
| 1368 |
if (isNormalized) {
|
| 1369 |
+
const dataX = (newCanvasX - fit.offsetX) / fit.scale;
|
| 1370 |
+
const dataY = (newCanvasY - fit.offsetY) / fit.scale;
|
| 1371 |
targetKeypoints[i] = dataX / dataResolutionWidth;
|
| 1372 |
targetKeypoints[i + 1] = dataY / dataResolutionHeight;
|
| 1373 |
} else {
|
| 1374 |
+
targetKeypoints[i] = (newCanvasX - fit.offsetX) / fit.scale;
|
| 1375 |
+
targetKeypoints[i + 1] = (newCanvasY - fit.offsetY) / fit.scale;
|
| 1376 |
}
|
| 1377 |
}
|
| 1378 |
}
|
|
|
|
| 1427 |
}
|
| 1428 |
const canvasWidth = window.poseEditorGlobals.canvas ? window.poseEditorGlobals.canvas.width : 512;
|
| 1429 |
const canvasHeight = window.poseEditorGlobals.canvas ? window.poseEditorGlobals.canvas.height : 512;
|
| 1430 |
+
// レターボックス対応フィット
|
| 1431 |
+
const fit = getFitParams([dataResolutionWidth, dataResolutionHeight]);
|
| 1432 |
|
| 1433 |
// Canvasの移動量→データ座標の移動量へ変換
|
| 1434 |
+
const dataDeltaX = deltaX / fit.scale;
|
| 1435 |
+
const dataDeltaY = deltaY / fit.scale;
|
| 1436 |
|
| 1437 |
// すべてのキーポイントを移動(データ座標系)
|
| 1438 |
for (let i = 0; i < keypoints.length; i += 3) {
|
|
|
|
| 1607 |
|
| 1608 |
// 📐 解像度情報の取得
|
| 1609 |
const originalRes = currentPoseData.resolution || [512, 512];
|
| 1610 |
+
// Canvas→データ変換(レターボックス対応)
|
| 1611 |
+
const d = canvasToDataXY(canvasX, canvasY, originalRes);
|
| 1612 |
+
const clampedDataX = d.x;
|
| 1613 |
+
const clampedDataY = d.y;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1614 |
|
| 1615 |
// 1. candidateリストを更新
|
| 1616 |
const candidates = currentPoseData.bodies.candidate;
|
|
|
|
| 1900 |
|
| 1901 |
// 📐 解像度情報の取得(手と顔描画のため)
|
| 1902 |
const originalRes = currentPoseData.resolution || [512, 512];
|
| 1903 |
+
const fit = getFitParams(originalRes);
|
|
|
|
| 1904 |
|
| 1905 |
|
| 1906 |
// ボディの描画(ハイライト対応)
|
|
|
|
| 1935 |
|
| 1936 |
if (handsDataForDrawing && handsDataForDrawing.length >= 2) {
|
| 1937 |
if (window.poseEditorDebug.hands) console.log('✅ Calling drawHands function');
|
| 1938 |
+
drawHands(handsDataForDrawing, originalRes);
|
| 1939 |
} else {
|
| 1940 |
if (window.poseEditorDebug.hands) console.log('❌ Invalid hands data for drawing');
|
| 1941 |
}
|
|
|
|
| 1953 |
person.hand_left_keypoints_2d || [],
|
| 1954 |
person.hand_right_keypoints_2d || []
|
| 1955 |
];
|
| 1956 |
+
drawHandRectangles(editedHandsData, originalRes);
|
| 1957 |
} else {
|
| 1958 |
// 💖 people形式のみサポート、空データで矩形なし
|
| 1959 |
+
drawHandRectangles([[], []], originalRes);
|
| 1960 |
}
|
| 1961 |
} else {
|
| 1962 |
// 編集モード中:既存の矩形を描画(再計算しない)
|
|
|
|
| 1983 |
}
|
| 1984 |
|
| 1985 |
if (facesDataForDrawing && facesDataForDrawing.length > 0) {
|
| 1986 |
+
drawFaces(facesDataForDrawing, originalRes);
|
| 1987 |
}
|
| 1988 |
} catch (error) {
|
| 1989 |
console.error("❌ Error drawing face:", error);
|
|
|
|
| 1995 |
// 🚀 通常時:編集済みキーポイントから矩形を計算
|
| 1996 |
if (currentPoseData.people && currentPoseData.people[0] && currentPoseData.people[0].face_keypoints_2d) {
|
| 1997 |
const editedFacesData = [currentPoseData.people[0].face_keypoints_2d];
|
| 1998 |
+
drawFaceRectangles(editedFacesData, originalRes);
|
| 1999 |
} else {
|
| 2000 |
+
drawFaceRectangles(currentPoseData.faces, originalRes);
|
| 2001 |
}
|
| 2002 |
} else {
|
| 2003 |
// 編集モード中:既存の矩形を描画(再計算しない)
|
|
|
|
| 2021 |
ctx.fillStyle = '#ffffff';
|
| 2022 |
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
| 2023 |
|
| 2024 |
+
// 背景画像がある場合はアスペクト比維持でフィット(上下黒帯 or 左右黒帯)
|
| 2025 |
if (window.poseEditorGlobals.backgroundImage) {
|
| 2026 |
const img = window.poseEditorGlobals.backgroundImage;
|
|
|
|
|
|
|
| 2027 |
const imgAspect = img.width / img.height;
|
| 2028 |
const canvasAspect = canvas.width / canvas.height;
|
|
|
|
| 2029 |
let drawWidth, drawHeight, offsetX, offsetY;
|
|
|
|
| 2030 |
if (imgAspect > canvasAspect) {
|
| 2031 |
+
// 画像の方が横長 → 幅をCanvasに合わせて上下黒帯
|
| 2032 |
drawWidth = canvas.width;
|
| 2033 |
+
drawHeight = Math.round(canvas.width / imgAspect);
|
| 2034 |
offsetX = 0;
|
| 2035 |
+
offsetY = Math.round((canvas.height - drawHeight) / 2);
|
| 2036 |
} else {
|
| 2037 |
+
// 画像の方が縦長 → 高さをCanvasに合わせて左右黒帯
|
| 2038 |
drawHeight = canvas.height;
|
| 2039 |
+
drawWidth = Math.round(canvas.height * imgAspect);
|
| 2040 |
+
offsetX = Math.round((canvas.width - drawWidth) / 2);
|
| 2041 |
offsetY = 0;
|
| 2042 |
}
|
|
|
|
|
|
|
| 2043 |
ctx.globalAlpha = 0.3;
|
| 2044 |
ctx.drawImage(img, offsetX, offsetY, drawWidth, drawHeight);
|
| 2045 |
+
ctx.globalAlpha = 1.0;
|
| 2046 |
}
|
| 2047 |
}
|
| 2048 |
|
|
|
|
| 2082 |
|
| 2083 |
// 📐 解像度情報の取得
|
| 2084 |
const originalRes = poseData.resolution || [512, 512];
|
| 2085 |
+
const fit = getFitParams(originalRes);
|
|
|
|
| 2086 |
|
| 2087 |
// 接続線の描画(refs互換・配列ベース + 座標変換)
|
| 2088 |
ctx.lineWidth = 3;
|
|
|
|
| 2102 |
startPoint[0] < originalRes[0] && startPoint[1] < originalRes[1] &&
|
| 2103 |
endPoint[0] < originalRes[0] && endPoint[1] < originalRes[1]) {
|
| 2104 |
|
| 2105 |
+
// 🔄 座標変換を適用(レターボックス対応)
|
| 2106 |
+
const startX = fit.offsetX + startPoint[0] * fit.scale;
|
| 2107 |
+
const startY = fit.offsetY + startPoint[1] * fit.scale;
|
| 2108 |
+
const endX = fit.offsetX + endPoint[0] * fit.scale;
|
| 2109 |
+
const endY = fit.offsetY + endPoint[1] * fit.scale;
|
| 2110 |
|
| 2111 |
// 🔧 refs互換: SKELETON_COLORSの配列ベース色分け
|
| 2112 |
ctx.strokeStyle = SKELETON_COLORS[i % SKELETON_COLORS.length];
|
|
|
|
| 2132 |
if (point && point[0] > 1 && point[1] > 1 &&
|
| 2133 |
point[0] < originalRes[0] && point[1] < originalRes[1]) {
|
| 2134 |
|
| 2135 |
+
// 🔄 座標変換を適用(レターボックス対応)
|
| 2136 |
+
const scaledX = fit.offsetX + point[0] * fit.scale;
|
| 2137 |
+
const scaledY = fit.offsetY + point[1] * fit.scale;
|
| 2138 |
|
| 2139 |
// 🔧 ハイライト対応: ドラッグ中のキーポイントを強調表示
|
| 2140 |
if (i === highlightIndex) {
|
|
|
|
| 2159 |
|
| 2160 |
// 🎨 補間機能: 有効キーポイントが少ない場合の視覚的改善
|
| 2161 |
if (drawnKeypoints < 10) {
|
| 2162 |
+
// console.log(`[DEBUG] 💡 Low keypoint count (${drawnKeypoints}), applying visual enhancements`);
|
| 2163 |
+
drawEstimatedConnections(candidates, originalRes);
|
| 2164 |
}
|
| 2165 |
}
|
| 2166 |
|
|
|
|
| 2173 |
}
|
| 2174 |
|
| 2175 |
// 手の描画(21キーポイント × 2)- refs互換
|
| 2176 |
+
function drawHands(handsData, originalRes, scaleX_unused, scaleY_unused) {
|
| 2177 |
if (!handsData || handsData.length === 0) return;
|
| 2178 |
|
| 2179 |
// console.log(`[DEBUG] 👋 Drawing hands with ${handsData.length} hand(s) - refs互換`);
|
|
|
|
| 2203 |
const conf = hand[i + 2];
|
| 2204 |
|
| 2205 |
if (conf > 0.1) { // refs互換の閾値
|
| 2206 |
+
// 座標変換を適用(レターボックス対応)
|
| 2207 |
+
const pt = dataToCanvasXY(x, y, originalRes);
|
| 2208 |
+
const scaledX = pt.x;
|
| 2209 |
+
const scaledY = pt.y;
|
| 2210 |
handKeypoints.push([scaledX, scaledY, conf]);
|
| 2211 |
} else {
|
| 2212 |
handKeypoints.push([0, 0, 0]); // 無効キーポイント
|
|
|
|
| 2267 |
}
|
| 2268 |
|
| 2269 |
// 顔の描画(68キーポイント)- refs互換
|
| 2270 |
+
function drawFaces(facesData, originalRes, scaleX_unused, scaleY_unused) {
|
| 2271 |
if (!facesData || facesData.length === 0) return;
|
| 2272 |
|
| 2273 |
// console.log(`[DEBUG] 👤 Drawing faces with ${facesData.length} face(s) - refs互換`);
|
|
|
|
| 2282 |
const conf = face[i + 2];
|
| 2283 |
|
| 2284 |
if (conf > 0.1) { // refs互換の閾値
|
| 2285 |
+
// 座標変換を適用(レターボックス対応)
|
| 2286 |
+
const pt = dataToCanvasXY(x, y, originalRes);
|
| 2287 |
+
const scaledX = pt.x;
|
| 2288 |
+
const scaledY = pt.y;
|
| 2289 |
faceKeypoints.push([scaledX, scaledY, conf]);
|
| 2290 |
} else {
|
| 2291 |
faceKeypoints.push([0, 0, 0]); // 無効キーポイント
|
|
|
|
| 2352 |
}
|
| 2353 |
};
|
| 2354 |
|
| 2355 |
+
// レターボックス対応のフィット変換(アスペクト比維持・黒帯)
|
| 2356 |
+
function getFitParams(originalRes) {
|
| 2357 |
+
const dataW = (originalRes && originalRes[0]) || 512;
|
| 2358 |
+
const dataH = (originalRes && originalRes[1]) || 512;
|
| 2359 |
+
const cw = canvas.width;
|
| 2360 |
+
const ch = canvas.height;
|
| 2361 |
+
const s = Math.min(cw / dataW, ch / dataH);
|
| 2362 |
+
const drawW = dataW * s;
|
| 2363 |
+
const drawH = dataH * s;
|
| 2364 |
+
const offsetX = (cw - drawW) / 2;
|
| 2365 |
+
const offsetY = (ch - drawH) / 2;
|
| 2366 |
+
return { scale: s, offsetX, offsetY };
|
| 2367 |
+
}
|
| 2368 |
+
|
| 2369 |
+
function dataToCanvasXY(x, y, originalRes) {
|
| 2370 |
+
const { scale, offsetX, offsetY } = getFitParams(originalRes);
|
| 2371 |
+
return {
|
| 2372 |
+
x: offsetX + x * scale,
|
| 2373 |
+
y: offsetY + y * scale
|
| 2374 |
+
};
|
| 2375 |
+
}
|
| 2376 |
+
|
| 2377 |
+
function canvasToDataXY(cx, cy, originalRes) {
|
| 2378 |
+
const dataW = (originalRes && originalRes[0]) || 512;
|
| 2379 |
+
const dataH = (originalRes && originalRes[1]) || 512;
|
| 2380 |
+
const { scale, offsetX, offsetY } = getFitParams(originalRes);
|
| 2381 |
+
const x = (cx - offsetX) / scale;
|
| 2382 |
+
const y = (cy - offsetY) / scale;
|
| 2383 |
+
return {
|
| 2384 |
+
x: Math.max(0, Math.min(dataW, x)),
|
| 2385 |
+
y: Math.max(0, Math.min(dataH, y))
|
| 2386 |
+
};
|
| 2387 |
+
}
|
| 2388 |
+
|
| 2389 |
// データ解像度とCanvas表示サイズの変換(後方互換性)
|
| 2390 |
function transformCoordinate(x, y, dataWidth, dataHeight) {
|
| 2391 |
const scaleX = canvas.width / dataWidth;
|
|
|
|
| 2399 |
|
| 2400 |
// 描画時に座標変換を適用
|
| 2401 |
function drawKeypointScaled(x, y, dataRes, radius = KEYPOINT_RADIUS) {
|
| 2402 |
+
const scaled = dataToCanvasXY(x, y, dataRes);
|
| 2403 |
drawKeypoint(scaled.x, scaled.y, radius);
|
| 2404 |
}
|
| 2405 |
|
| 2406 |
// Canvas解像度更新
|
| 2407 |
+
// 既存キーポイントを新しいサイズへ変換(正規化スケーリング)
|
| 2408 |
+
function updateKeypointsForNewSize(p, oldW, oldH, newW, newH) {
|
| 2409 |
+
if (!p) return;
|
| 2410 |
+
const oW = Math.max(1, oldW || 512);
|
| 2411 |
+
const oH = Math.max(1, oldH || 512);
|
| 2412 |
+
const nW = Math.max(1, newW || oW);
|
| 2413 |
+
const nH = Math.max(1, newH || oH);
|
| 2414 |
+
|
| 2415 |
+
function scaleXY(x, y) {
|
| 2416 |
+
// 正規化検出(0〜1範囲なら正規化座標とみなす)
|
| 2417 |
+
const isNorm = (x >= 0 && x <= 1 && y >= 0 && y <= 1);
|
| 2418 |
+
const nx = isNorm ? x * nW : (x / oW) * nW;
|
| 2419 |
+
const ny = isNorm ? y * nH : (y / oH) * nH;
|
| 2420 |
+
return [nx, ny];
|
| 2421 |
+
}
|
| 2422 |
+
|
| 2423 |
+
// bodies.candidate: [[x,y,conf,...], ...]
|
| 2424 |
+
try {
|
| 2425 |
+
if (p.bodies && Array.isArray(p.bodies.candidate)) {
|
| 2426 |
+
for (let i = 0; i < p.bodies.candidate.length; i++) {
|
| 2427 |
+
const pt = p.bodies.candidate[i];
|
| 2428 |
+
if (pt && pt.length >= 2) {
|
| 2429 |
+
const [nx, ny] = scaleXY(pt[0], pt[1]);
|
| 2430 |
+
p.bodies.candidate[i][0] = nx;
|
| 2431 |
+
p.bodies.candidate[i][1] = ny;
|
| 2432 |
+
}
|
| 2433 |
+
}
|
| 2434 |
+
}
|
| 2435 |
+
} catch (e) { /* no-op */ }
|
| 2436 |
+
|
| 2437 |
+
// people[0].pose_keypoints_2d: [x,y,conf, ...]
|
| 2438 |
+
try {
|
| 2439 |
+
if (p.people && p.people[0] && Array.isArray(p.people[0].pose_keypoints_2d)) {
|
| 2440 |
+
const arr = p.people[0].pose_keypoints_2d;
|
| 2441 |
+
for (let i = 0; i < arr.length; i += 3) {
|
| 2442 |
+
if (i + 1 < arr.length) {
|
| 2443 |
+
const [nx, ny] = scaleXY(arr[i], arr[i + 1]);
|
| 2444 |
+
arr[i] = nx; arr[i + 1] = ny;
|
| 2445 |
+
}
|
| 2446 |
+
}
|
| 2447 |
+
}
|
| 2448 |
+
} catch (e) { /* no-op */ }
|
| 2449 |
+
|
| 2450 |
+
// hands (people形式優先)
|
| 2451 |
+
try {
|
| 2452 |
+
if (p.people && p.people[0]) {
|
| 2453 |
+
const person = p.people[0];
|
| 2454 |
+
const handFields = ['hand_left_keypoints_2d', 'hand_right_keypoints_2d'];
|
| 2455 |
+
for (const field of handFields) {
|
| 2456 |
+
if (Array.isArray(person[field])) {
|
| 2457 |
+
for (let i = 0; i < person[field].length; i += 3) {
|
| 2458 |
+
if (i + 1 < person[field].length) {
|
| 2459 |
+
const [nx, ny] = scaleXY(person[field][i], person[field][i + 1]);
|
| 2460 |
+
person[field][i] = nx; person[field][i + 1] = ny;
|
| 2461 |
+
}
|
| 2462 |
+
}
|
| 2463 |
+
}
|
| 2464 |
+
}
|
| 2465 |
+
}
|
| 2466 |
+
} catch (e) { /* no-op */ }
|
| 2467 |
+
|
| 2468 |
+
// faces (people形式優先)
|
| 2469 |
+
try {
|
| 2470 |
+
if (p.people && p.people[0] && Array.isArray(p.people[0].face_keypoints_2d)) {
|
| 2471 |
+
const arr = p.people[0].face_keypoints_2d;
|
| 2472 |
+
for (let i = 0; i < arr.length; i += 3) {
|
| 2473 |
+
if (i + 1 < arr.length) {
|
| 2474 |
+
const [nx, ny] = scaleXY(arr[i], arr[i + 1]);
|
| 2475 |
+
arr[i] = nx; arr[i + 1] = ny;
|
| 2476 |
+
}
|
| 2477 |
+
}
|
| 2478 |
+
}
|
| 2479 |
+
} catch (e) { /* no-op */ }
|
| 2480 |
+
|
| 2481 |
+
// 旧形式 hands/faces も同期
|
| 2482 |
+
try {
|
| 2483 |
+
if (Array.isArray(p.hands)) {
|
| 2484 |
+
for (let h = 0; h < p.hands.length; h++) {
|
| 2485 |
+
const hand = p.hands[h];
|
| 2486 |
+
if (Array.isArray(hand)) {
|
| 2487 |
+
for (let i = 0; i < hand.length; i += 3) {
|
| 2488 |
+
if (i + 1 < hand.length) {
|
| 2489 |
+
const [nx, ny] = scaleXY(hand[i], hand[i + 1]);
|
| 2490 |
+
hand[i] = nx; hand[i + 1] = ny;
|
| 2491 |
+
}
|
| 2492 |
+
}
|
| 2493 |
+
}
|
| 2494 |
+
}
|
| 2495 |
+
}
|
| 2496 |
+
} catch (e) { /* no-op */ }
|
| 2497 |
+
try {
|
| 2498 |
+
if (Array.isArray(p.faces)) {
|
| 2499 |
+
for (let f = 0; f < p.faces.length; f++) {
|
| 2500 |
+
const face = p.faces[f];
|
| 2501 |
+
if (Array.isArray(face)) {
|
| 2502 |
+
for (let i = 0; i < face.length; i += 3) {
|
| 2503 |
+
if (i + 1 < face.length) {
|
| 2504 |
+
const [nx, ny] = scaleXY(face[i], face[i + 1]);
|
| 2505 |
+
face[i] = nx; face[i + 1] = ny;
|
| 2506 |
+
}
|
| 2507 |
+
}
|
| 2508 |
+
}
|
| 2509 |
+
}
|
| 2510 |
+
}
|
| 2511 |
+
} catch (e) { /* no-op */ }
|
| 2512 |
+
|
| 2513 |
+
// 解像度メタ更新
|
| 2514 |
+
p.resolution = [nW, nH];
|
| 2515 |
+
if (!p.metadata) p.metadata = {};
|
| 2516 |
+
p.metadata.resolution = [nW, nH];
|
| 2517 |
+
}
|
| 2518 |
+
|
| 2519 |
+
// 出力フォームとposeData.resolutionの不一致を検出してデータ側をスケール
|
| 2520 |
+
function alignPoseDataToOutputForm() {
|
| 2521 |
+
try {
|
| 2522 |
+
const p = window.poseEditorGlobals.poseData || poseData;
|
| 2523 |
+
if (!p) return false;
|
| 2524 |
+
// 取得: 現在のデータ解像度
|
| 2525 |
+
const curRes = (p.resolution || (p.metadata && p.metadata.resolution)) || [512,512];
|
| 2526 |
+
let curW = curRes[0] || 512, curH = curRes[1] || 512;
|
| 2527 |
+
// 取得: フォームの出力サイズ
|
| 2528 |
+
const nums = Array.from(document.querySelectorAll('input[type="number"]'));
|
| 2529 |
+
let outW = null, outH = null;
|
| 2530 |
+
for (const el of nums) {
|
| 2531 |
+
const label = el.labels && el.labels[0] ? (el.labels[0].textContent || '').trim() : '';
|
| 2532 |
+
if (label.includes('幅')) outW = parseInt(el.value || '0');
|
| 2533 |
+
if (label.includes('高さ')) outH = parseInt(el.value || '0');
|
| 2534 |
+
}
|
| 2535 |
+
if (!outW || !outH) return false;
|
| 2536 |
+
// すでに一致していれば何もしない
|
| 2537 |
+
if (curW === outW && curH === outH) return false;
|
| 2538 |
+
// データを新サイズへスケール
|
| 2539 |
+
updateKeypointsForNewSize(p, curW, curH, outW, outH);
|
| 2540 |
+
// 解像度メタも更新
|
| 2541 |
+
p.resolution = [outW, outH];
|
| 2542 |
+
if (!p.metadata) p.metadata = {};
|
| 2543 |
+
p.metadata.resolution = [outW, outH];
|
| 2544 |
+
// 参照を戻す
|
| 2545 |
+
poseData = p;
|
| 2546 |
+
window.poseEditorGlobals.poseData = p;
|
| 2547 |
+
return true;
|
| 2548 |
+
} catch (e) { return false; }
|
| 2549 |
+
}
|
| 2550 |
+
|
| 2551 |
+
// Canvas解像度更新(座標���新サイズへスケール)
|
| 2552 |
function updateCanvasResolution(width, height) {
|
| 2553 |
if (!canvas) return false;
|
| 2554 |
+
|
| 2555 |
+
const newW = Math.max(1, Math.floor(width));
|
| 2556 |
+
const newH = Math.max(1, Math.floor(height));
|
| 2557 |
+
|
| 2558 |
+
// 旧データ解像度を取得
|
| 2559 |
+
const current = window.poseEditorGlobals.poseData || poseData;
|
| 2560 |
+
const oldRes = (current && (current.resolution || (current.metadata && current.metadata.resolution))) || [512, 512];
|
| 2561 |
+
const oldW = oldRes[0] || 512;
|
| 2562 |
+
const oldH = oldRes[1] || 512;
|
| 2563 |
+
|
| 2564 |
+
// 表示Canvasは「出力比率」に揃える(解像度ではなく比率がポイント)
|
| 2565 |
+
const base = 640; // 表示上の基準長辺
|
| 2566 |
+
let dispW, dispH;
|
| 2567 |
+
if (newW >= newH) {
|
| 2568 |
+
dispW = base;
|
| 2569 |
+
dispH = Math.max(1, Math.round(base * (newH / newW)));
|
| 2570 |
+
} else {
|
| 2571 |
+
dispH = base;
|
| 2572 |
+
dispW = Math.max(1, Math.round(base * (newW / newH)));
|
| 2573 |
+
}
|
| 2574 |
+
|
| 2575 |
+
// Canvasサイズ更新(表示)
|
| 2576 |
+
canvas.width = dispW;
|
| 2577 |
+
canvas.height = dispH;
|
| 2578 |
+
|
| 2579 |
+
// ディスプレイ座標系更新
|
| 2580 |
+
coordinateTransformer.updateResolution(null, [dispW, dispH]);
|
| 2581 |
+
|
| 2582 |
+
// 既存データを新サイズにスケール
|
| 2583 |
+
if (current) {
|
| 2584 |
+
updateKeypointsForNewSize(current, oldW, oldH, newW, newH);
|
| 2585 |
+
// 参照も同期
|
| 2586 |
+
poseData = current;
|
| 2587 |
+
window.poseEditorGlobals.poseData = current;
|
| 2588 |
+
}
|
| 2589 |
+
|
| 2590 |
+
// フォームに合わせて(万一)比率を再調整
|
| 2591 |
+
ensureCanvasAspectFromOutputForm(base);
|
| 2592 |
+
|
| 2593 |
+
// 再描画
|
| 2594 |
if (poseData) {
|
| 2595 |
+
drawPose(poseData, window.poseEditorGlobals.enableHands, window.poseEditorGlobals.enableFace);
|
| 2596 |
+
} else {
|
| 2597 |
+
clearCanvas();
|
| 2598 |
}
|
| 2599 |
+
|
| 2600 |
+
notifyCanvasOperation(`Canvas解像度(表示)を${dispW}x${dispH}に、データを${newW}x${newH}に変更しました`);
|
| 2601 |
+
// 🔄 サーバーへ最新データを送信してエクスポートと同期
|
| 2602 |
+
try {
|
| 2603 |
+
if (typeof sendPoseDataToGradio === 'function') {
|
| 2604 |
+
// 次の描画フレーム後に送信してループを回避
|
| 2605 |
+
setTimeout(() => { try { sendPoseDataToGradio(); } catch (e) {} }, 0);
|
| 2606 |
+
}
|
| 2607 |
+
} catch (e) {}
|
| 2608 |
+
return true;
|
| 2609 |
+
}
|
| 2610 |
+
|
| 2611 |
+
// Canvas解像度更新(データはスケールせず、表示のみ変更)
|
| 2612 |
+
function setCanvasSizeNoScale(width, height) {
|
| 2613 |
+
if (!canvas) return false;
|
| 2614 |
+
const newW = Math.max(1, Math.floor(width));
|
| 2615 |
+
const newH = Math.max(1, Math.floor(height));
|
| 2616 |
+
canvas.width = newW;
|
| 2617 |
+
canvas.height = newH;
|
| 2618 |
+
coordinateTransformer.updateResolution(null, [newW, newH]);
|
| 2619 |
+
if (poseData) {
|
| 2620 |
+
drawPose(poseData, window.poseEditorGlobals.enableHands, window.poseEditorGlobals.enableFace);
|
| 2621 |
+
} else {
|
| 2622 |
+
clearCanvas();
|
| 2623 |
+
}
|
| 2624 |
+
notifyCanvasOperation(`Canvas表示サイズを${newW}x${newH}に変更しました(データはスケールしません)`);
|
| 2625 |
return true;
|
| 2626 |
}
|
| 2627 |
|
|
|
|
| 2704 |
window.poseEditorGlobals.poseData = poseData;
|
| 2705 |
}
|
| 2706 |
|
| 2707 |
+
// 💖 表示Canvasを出力比率に自動追従
|
| 2708 |
+
try {
|
| 2709 |
+
const c = window.poseEditorGlobals.canvas || document.getElementById('pose_canvas');
|
| 2710 |
+
if (c) {
|
| 2711 |
+
const res = (poseData && (poseData.resolution || (poseData.metadata && poseData.metadata.resolution))) || [512,512];
|
| 2712 |
+
const outW = Math.max(1, Math.floor(res[0] || 512));
|
| 2713 |
+
const outH = Math.max(1, Math.floor(res[1] || 512));
|
| 2714 |
+
const base = 640; // 長辺基準
|
| 2715 |
+
let dispW, dispH;
|
| 2716 |
+
if (outW >= outH) { dispW = base; dispH = Math.max(1, Math.round(base * (outH/outW))); }
|
| 2717 |
+
else { dispH = base; dispW = Math.max(1, Math.round(base * (outW/outH))); }
|
| 2718 |
+
if (c.width !== dispW || c.height !== dispH) {
|
| 2719 |
+
c.width = dispW; c.height = dispH;
|
| 2720 |
+
window.poseEditorGlobals.canvas = c;
|
| 2721 |
+
window.poseEditorGlobals.ctx = c.getContext('2d');
|
| 2722 |
+
}
|
| 2723 |
+
// 変換器の表示解像度も更新
|
| 2724 |
+
if (typeof coordinateTransformer?.updateResolution === 'function') {
|
| 2725 |
+
coordinateTransformer.updateResolution(null, [c.width, c.height]);
|
| 2726 |
+
}
|
| 2727 |
+
}
|
| 2728 |
+
} catch(e) {}
|
| 2729 |
+
|
| 2730 |
// 💖 originalKeypointsも設定(但し、baseOriginalKeypointsは保護)
|
| 2731 |
if (poseData && poseData.people && poseData.people[0]) {
|
| 2732 |
// baseOriginalKeypointsは上書きしない(編集セッション保持のため)
|
|
|
|
| 2776 |
|
| 2777 |
// グローバル設定で描画(手・顔表示設定を反映)
|
| 2778 |
if (poseData && Object.keys(poseData).length > 0) {
|
| 2779 |
+
// 表示Canvasはフォーム比率に追従
|
| 2780 |
+
ensureCanvasAspectFromOutputForm(640);
|
| 2781 |
+
// 受信直後にデータ解像度をフォーム値に合わせてスケール(初回ズレ防止)
|
| 2782 |
+
alignPoseDataToOutputForm();
|
| 2783 |
drawPose(
|
| 2784 |
poseData,
|
| 2785 |
currentHandsEnabled,
|
|
|
|
| 2956 |
|
| 2957 |
|
| 2958 |
// 🔧 簡易モード:手の矩形描画(refs互換)
|
| 2959 |
+
function drawHandRectangles(handsData, originalRes, scaleX_unused, scaleY_unused) {
|
| 2960 |
if (!handsData || handsData.length === 0) return;
|
| 2961 |
|
| 2962 |
|
|
|
|
| 2968 |
|
| 2969 |
handsData.forEach((hand, handIndex) => {
|
| 2970 |
if (hand && hand.length > 0) {
|
| 2971 |
+
const rect = calculateHandRect(hand, originalRes);
|
| 2972 |
if (rect) {
|
| 2973 |
const handType = HAND_TYPES[handIndex] || `hand_${handIndex}`;
|
| 2974 |
|
|
|
|
| 2987 |
}
|
| 2988 |
|
| 2989 |
// 🔧 簡易モード:顔の矩形描画(refs互換)
|
| 2990 |
+
function drawFaceRectangles(facesData, originalRes, scaleX_unused, scaleY_unused) {
|
| 2991 |
if (!facesData || facesData.length === 0) return;
|
| 2992 |
|
| 2993 |
|
|
|
|
| 2998 |
|
| 2999 |
const face = facesData[0]; // 最初の顔のみ(編集済みデータ)
|
| 3000 |
if (face && face.length > 0) {
|
| 3001 |
+
const rect = calculateFaceRect(face, originalRes);
|
| 3002 |
if (rect) {
|
| 3003 |
// 🔧 グローバルに矩形情報保存(編集モード中は既存の矩形を保持)
|
| 3004 |
if (!window.poseEditorGlobals.currentRects.face || !window.poseEditorGlobals.rectEditModeActive) {
|
|
|
|
| 3025 |
const confidence = handData[i + 2];
|
| 3026 |
|
| 3027 |
if (confidence > 0.3) { // refs互換の閾値
|
| 3028 |
+
// 🔧 レターボックス対応の座標変換
|
| 3029 |
+
const pt = dataToCanvasXY(x, y, originalRes);
|
| 3030 |
+
const finalX = pt.x;
|
| 3031 |
+
const finalY = pt.y;
|
|
|
|
| 3032 |
|
| 3033 |
minX = Math.min(minX, finalX);
|
| 3034 |
minY = Math.min(minY, finalY);
|
|
|
|
| 3111 |
const confidence = faceData[i + 2];
|
| 3112 |
|
| 3113 |
if (confidence > 0.3) { // refs互換の閾値
|
| 3114 |
+
const pt = dataToCanvasXY(x, y, originalRes);
|
| 3115 |
+
const scaledX = pt.x;
|
| 3116 |
+
const scaledY = pt.y;
|
| 3117 |
|
| 3118 |
minX = Math.min(minX, scaledX);
|
| 3119 |
minY = Math.min(minY, scaledY);
|
|
|
|
| 3211 |
|
| 3212 |
// 📐 解像度情報の取得
|
| 3213 |
const originalRes = currentPoseData.resolution || [512, 512];
|
| 3214 |
+
const fit = getFitParams(originalRes);
|
|
|
|
| 3215 |
|
| 3216 |
// ボディの描画(ハイライトなし)
|
| 3217 |
drawBody(currentPoseData, -1);
|
|
|
|
| 3224 |
person.hand_left_keypoints_2d || [],
|
| 3225 |
person.hand_right_keypoints_2d || []
|
| 3226 |
];
|
| 3227 |
+
drawHands(handsData, originalRes);
|
| 3228 |
}
|
| 3229 |
|
| 3230 |
// 顔の描画(設定制御・座標変換パラメータ付き)
|
| 3231 |
if (window.poseEditorGlobals.enableFace && currentPoseData.faces) {
|
| 3232 |
+
drawFaces(currentPoseData.faces, originalRes);
|
| 3233 |
}
|
| 3234 |
|
| 3235 |
// 🔧 既存の矩形のみ描画(再計算なし)
|
|
|
|
| 3600 |
const origY = originalKeypointsArray[i + 1];
|
| 3601 |
|
| 3602 |
if (origX > 0 && origY > 0) {
|
| 3603 |
+
// 🚀 座標変換:Canvas↔データ座標(レターボックス対応)
|
| 3604 |
+
const res = (window.poseEditorGlobals.poseData && window.poseEditorGlobals.poseData.resolution) || [512,512];
|
| 3605 |
+
const fit = getFitParams(res);
|
| 3606 |
+
const origRectDataX = (originalRect.x - fit.offsetX) / fit.scale;
|
| 3607 |
+
const origRectDataY = (originalRect.y - fit.offsetY) / fit.scale;
|
| 3608 |
+
const newRectDataX = (newRect.x - fit.offsetX) / fit.scale;
|
| 3609 |
+
const newRectDataY = (newRect.y - fit.offsetY) / fit.scale;
|
| 3610 |
+
const origRectWidthData = originalRect.width / fit.scale;
|
| 3611 |
+
const origRectHeightData = originalRect.height / fit.scale;
|
| 3612 |
+
const newRectWidthData = newRect.width / fit.scale;
|
| 3613 |
+
const newRectHeightData = newRect.height / fit.scale;
|
|
|
|
| 3614 |
|
| 3615 |
// 元矩形内の相対位置を計算
|
| 3616 |
const relativeX = (origX - origRectDataX) / origRectWidthData;
|
|
|
|
| 3659 |
const currentX = keypointsArray[i];
|
| 3660 |
const currentY = keypointsArray[i + 1];
|
| 3661 |
|
| 3662 |
+
// 🚀 座標変換:表示座標の移動量→データ座標の移動量(レタボ対応)
|
| 3663 |
+
const res = (window.poseEditorGlobals.poseData && window.poseEditorGlobals.poseData.resolution) || [512,512];
|
| 3664 |
+
const fit = getFitParams(res);
|
| 3665 |
+
const dataDeltaX = deltaX / fit.scale;
|
| 3666 |
+
const dataDeltaY = deltaY / fit.scale;
|
| 3667 |
|
| 3668 |
// 移動(512x512にクランプ)
|
| 3669 |
+
const dataW = res[0] || 512;
|
| 3670 |
+
const dataH = res[1] || 512;
|
| 3671 |
+
const newX = Math.max(0, Math.min(dataW, currentX + dataDeltaX));
|
| 3672 |
+
const newY = Math.max(0, Math.min(dataH, currentY + dataDeltaY));
|
| 3673 |
|
| 3674 |
keypointsArray[i] = newX;
|
| 3675 |
keypointsArray[i + 1] = newY;
|
|
|
|
| 3809 |
dataResolutionHeight = currentPoseData.resolution[1];
|
| 3810 |
}
|
| 3811 |
|
| 3812 |
+
const fit = getFitParams([dataResolutionWidth, dataResolutionHeight]);
|
|
|
|
| 3813 |
|
| 3814 |
// 正規化座標かピクセル座標かを判定(refs互換)
|
| 3815 |
let isNormalized = false;
|
|
|
|
| 3838 |
// データ座標→Canvas座標
|
| 3839 |
let canvasX, canvasY;
|
| 3840 |
if (isNormalized) {
|
| 3841 |
+
canvasX = fit.offsetX + (x * dataResolutionWidth) * fit.scale;
|
| 3842 |
+
canvasY = fit.offsetY + (y * dataResolutionHeight) * fit.scale;
|
| 3843 |
} else {
|
| 3844 |
+
canvasX = fit.offsetX + x * fit.scale;
|
| 3845 |
+
canvasY = fit.offsetY + y * fit.scale;
|
| 3846 |
}
|
| 3847 |
|
| 3848 |
// 🔧 元矩形内での相対位置を計算(refs互換・安全範囲チェック)
|
|
|
|
| 3859 |
|
| 3860 |
// 🔧 Canvas座標→データ座標に戻す(refs互換・範囲制限付き)
|
| 3861 |
if (isNormalized) {
|
| 3862 |
+
const dataX = (newCanvasX - fit.offsetX) / fit.scale;
|
| 3863 |
+
const dataY = (newCanvasY - fit.offsetY) / fit.scale;
|
| 3864 |
let newNormX = dataX / dataResolutionWidth;
|
| 3865 |
let newNormY = dataY / dataResolutionHeight;
|
| 3866 |
|
|
|
|
| 3871 |
targetKeypoints[i] = newNormX;
|
| 3872 |
targetKeypoints[i + 1] = newNormY;
|
| 3873 |
} else {
|
| 3874 |
+
let newDataX = (newCanvasX - fit.offsetX) / fit.scale;
|
| 3875 |
+
let newDataY = (newCanvasY - fit.offsetY) / fit.scale;
|
| 3876 |
|
| 3877 |
// ピクセル座標の範囲制限
|
| 3878 |
newDataX = Math.max(0, Math.min(dataResolutionWidth, newDataX));
|
|
|
|
| 3956 |
dataResolutionHeight = currentPoseData.resolution[1];
|
| 3957 |
}
|
| 3958 |
|
| 3959 |
+
const fit = getFitParams([dataResolutionWidth, dataResolutionHeight]);
|
|
|
|
| 3960 |
|
| 3961 |
// 正規化座標かピクセル座標かを判定(refs互換)
|
| 3962 |
let isNormalized = false;
|
|
|
|
| 3972 |
}
|
| 3973 |
|
| 3974 |
// Canvas座標での移動量をデータ座標での移動量に変換
|
| 3975 |
+
const dataDeltaX = deltaX / fit.scale;
|
| 3976 |
+
const dataDeltaY = deltaY / fit.scale;
|
| 3977 |
|
| 3978 |
|
| 3979 |
let movedCount = 0;
|
|
|
|
| 4000 |
}
|
| 4001 |
|
| 4002 |
// 🎨 推定接続の描画(少ないキーポイント用の補間機能)
|
| 4003 |
+
function drawEstimatedConnections(candidates, originalRes) {
|
| 4004 |
const ctx = window.poseEditorGlobals.ctx;
|
| 4005 |
+
const fit = getFitParams(originalRes);
|
| 4006 |
|
| 4007 |
// 有効なキーポイントを取得
|
| 4008 |
const validPoints = [];
|
|
|
|
| 4012 |
point[0] < originalRes[0] && point[1] < originalRes[1]) {
|
| 4013 |
validPoints.push({
|
| 4014 |
index: i,
|
| 4015 |
+
x: fit.offsetX + point[0] * fit.scale,
|
| 4016 |
+
y: fit.offsetY + point[1] * fit.scale,
|
| 4017 |
originalX: point[0],
|
| 4018 |
originalY: point[1]
|
| 4019 |
});
|
|
|
|
| 4057 |
}
|
| 4058 |
|
| 4059 |
// スタイルをリセット
|
| 4060 |
+
// 出力フォーム(幅/高さ)から比率を取得しCanvas表示サイズを合わせる
|
| 4061 |
+
function ensureCanvasAspectFromOutputForm(baseLongSide = 640) {
|
| 4062 |
+
try {
|
| 4063 |
+
const c = window.poseEditorGlobals.canvas || document.getElementById('pose_canvas');
|
| 4064 |
+
if (!c) return;
|
| 4065 |
+
// 「幅」「高さ」ラベル付きのnumber inputを探索
|
| 4066 |
+
const nums = Array.from(document.querySelectorAll('input[type="number"]'));
|
| 4067 |
+
let w = null, h = null;
|
| 4068 |
+
for (const el of nums) {
|
| 4069 |
+
const labelText = el.labels && el.labels[0] ? (el.labels[0].textContent || '').trim() : '';
|
| 4070 |
+
if (labelText.includes('幅')) w = parseInt(el.value || '0');
|
| 4071 |
+
if (labelText.includes('高さ')) h = parseInt(el.value || '0');
|
| 4072 |
+
}
|
| 4073 |
+
if (!w || !h || w <= 0 || h <= 0) return;
|
| 4074 |
+
// 比率に合わせて表示Canvasサイズを再計算
|
| 4075 |
+
let dispW, dispH;
|
| 4076 |
+
if (w >= h) { dispW = baseLongSide; dispH = Math.max(1, Math.round(baseLongSide * (h / w))); }
|
| 4077 |
+
else { dispH = baseLongSide; dispW = Math.max(1, Math.round(baseLongSide * (w / h))); }
|
| 4078 |
+
if (c.width !== dispW || c.height !== dispH) {
|
| 4079 |
+
c.width = dispW; c.height = dispH;
|
| 4080 |
+
window.poseEditorGlobals.canvas = c;
|
| 4081 |
+
window.poseEditorGlobals.ctx = c.getContext('2d');
|
| 4082 |
+
if (typeof coordinateTransformer?.updateResolution === 'function') {
|
| 4083 |
+
coordinateTransformer.updateResolution(null, [dispW, dispH]);
|
| 4084 |
+
}
|
| 4085 |
+
}
|
| 4086 |
+
} catch (e) {}
|
| 4087 |
+
}
|
| 4088 |
ctx.setLineDash([]); // 実線に戻す
|
| 4089 |
ctx.globalAlpha = 1.0; // 不透明に戻す
|
| 4090 |
}
|
| 4091 |
|
| 4092 |
// 🎨 pose_editor.js initialization complete
|
| 4093 |
+
// --- Global helper to enforce canvas aspect from output form ---
|
| 4094 |
+
(function(){
|
| 4095 |
+
if (!window.ensureCanvasAspectFromOutputForm) {
|
| 4096 |
+
window.ensureCanvasAspectFromOutputForm = function(baseLongSide = 640) {
|
| 4097 |
+
try {
|
| 4098 |
+
const c = window.poseEditorGlobals?.canvas || document.getElementById('pose_canvas');
|
| 4099 |
+
if (!c) return;
|
| 4100 |
+
// 1) try form values
|
| 4101 |
+
const nums = Array.from(document.querySelectorAll('input[type="number"]'));
|
| 4102 |
+
let w = null, h = null;
|
| 4103 |
+
for (const el of nums) {
|
| 4104 |
+
const label = el.labels && el.labels[0] ? (el.labels[0].textContent||'').trim() : '';
|
| 4105 |
+
if (label.includes('幅')) w = parseInt(el.value||'0');
|
| 4106 |
+
if (label.includes('高さ')) h = parseInt(el.value||'0');
|
| 4107 |
+
}
|
| 4108 |
+
// 2) fallback to pose resolution
|
| 4109 |
+
if (!w || !h) {
|
| 4110 |
+
const res = (window.poseEditorGlobals?.poseData?.resolution) || [512,512];
|
| 4111 |
+
w = res[0]; h = res[1];
|
| 4112 |
+
}
|
| 4113 |
+
if (!w || !h || w<=0 || h<=0) return;
|
| 4114 |
+
let dispW, dispH;
|
| 4115 |
+
if (w >= h) { dispW = baseLongSide; dispH = Math.max(1, Math.round(baseLongSide * (h/w))); }
|
| 4116 |
+
else { dispH = baseLongSide; dispW = Math.max(1, Math.round(baseLongSide * (w/h))); }
|
| 4117 |
+
if (c.width !== dispW || c.height !== dispH) {
|
| 4118 |
+
c.width = dispW; c.height = dispH;
|
| 4119 |
+
window.poseEditorGlobals.canvas = c;
|
| 4120 |
+
window.poseEditorGlobals.ctx = c.getContext('2d');
|
| 4121 |
+
if (typeof coordinateTransformer?.updateResolution === 'function') {
|
| 4122 |
+
coordinateTransformer.updateResolution(null, [dispW, dispH]);
|
| 4123 |
+
}
|
| 4124 |
+
}
|
| 4125 |
+
} catch(e) {}
|
| 4126 |
+
}
|
| 4127 |
+
}
|
| 4128 |
+
})();
|
utils/export_utils.py
CHANGED
|
@@ -8,6 +8,54 @@ 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 |
タイムスタンプ付きファイル名を生成
|
|
@@ -50,10 +98,30 @@ def export_pose_as_image(pose_data, canvas_size=(640, 640), background_color=(0,
|
|
| 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', []))}")
|
|
@@ -73,7 +141,7 @@ def export_pose_as_image(pose_data, canvas_size=(640, 640), background_color=(0,
|
|
| 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] ⚠️ 手描画スキップ - 手データなし")
|
|
@@ -92,7 +160,7 @@ def export_pose_as_image(pose_data, canvas_size=(640, 640), background_color=(0,
|
|
| 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] ⚠️ 顔描画スキップ - 顔データなし")
|
|
@@ -106,7 +174,7 @@ def export_pose_as_image(pose_data, canvas_size=(640, 640), background_color=(0,
|
|
| 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準拠)")
|
|
@@ -132,6 +200,9 @@ def draw_body_on_image(draw, pose_data, canvas_size):
|
|
| 132 |
]
|
| 133 |
|
| 134 |
W, H = canvas_size
|
|
|
|
|
|
|
|
|
|
| 135 |
detection_threshold = 0.3
|
| 136 |
|
| 137 |
for person in people:
|
|
@@ -147,12 +218,21 @@ def draw_body_on_image(draw, pose_data, canvas_size):
|
|
| 147 |
|
| 148 |
print(f"[DEBUG] 🎨 keypoints count: {len(keypoints)}")
|
| 149 |
|
| 150 |
-
#
|
| 151 |
-
|
|
|
|
| 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):
|
|
@@ -192,35 +272,47 @@ def draw_body_on_image(draw, pose_data, canvas_size):
|
|
| 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 |
-
# 💖
|
| 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 |
-
# 💖
|
| 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 |
|
|
@@ -349,4 +441,4 @@ def create_download_link(content, filename, content_type="text/plain"):
|
|
| 349 |
|
| 350 |
except Exception as e:
|
| 351 |
print(f"Download link creation error: {e}")
|
| 352 |
-
return None
|
|
|
|
| 8 |
from datetime import datetime
|
| 9 |
from .notifications import notify_success, notify_error
|
| 10 |
|
| 11 |
+
def _detect_source_resolution_from_data(pose_data):
|
| 12 |
+
"""推定的にデータ座標系の解像度を検出(最大x/yから推定)"""
|
| 13 |
+
try:
|
| 14 |
+
max_x = 0.0
|
| 15 |
+
max_y = 0.0
|
| 16 |
+
def scan_points(arr):
|
| 17 |
+
nonlocal max_x, max_y
|
| 18 |
+
if not arr: return
|
| 19 |
+
for i in range(0, len(arr), 3):
|
| 20 |
+
if i + 2 < len(arr):
|
| 21 |
+
x, y, conf = arr[i], arr[i+1], arr[i+2]
|
| 22 |
+
if conf is None or conf <= 0:
|
| 23 |
+
continue
|
| 24 |
+
# 正規化の可能性は別で判定するので、そのまま最大値を取る
|
| 25 |
+
if isinstance(x, (int, float)) and isinstance(y, (int, float)):
|
| 26 |
+
max_x = max(max_x, float(x))
|
| 27 |
+
max_y = max(max_y, float(y))
|
| 28 |
+
|
| 29 |
+
if isinstance(pose_data, dict) and 'people' in pose_data and pose_data['people']:
|
| 30 |
+
person = pose_data['people'][0]
|
| 31 |
+
scan_points(person.get('pose_keypoints_2d', []))
|
| 32 |
+
scan_points(person.get('hand_left_keypoints_2d', []))
|
| 33 |
+
scan_points(person.get('hand_right_keypoints_2d', []))
|
| 34 |
+
scan_points(person.get('face_keypoints_2d', []))
|
| 35 |
+
else:
|
| 36 |
+
# bodies/hands/faces 互換
|
| 37 |
+
if 'bodies' in pose_data and pose_data['bodies'] and 'candidate' in pose_data['bodies']:
|
| 38 |
+
cands = pose_data['bodies']['candidate'] or []
|
| 39 |
+
for c in cands:
|
| 40 |
+
if c and len(c) >= 2:
|
| 41 |
+
max_x = max(max_x, float(c[0]))
|
| 42 |
+
max_y = max(max_y, float(c[1]))
|
| 43 |
+
for hand in (pose_data.get('hands') or []):
|
| 44 |
+
scan_points(hand)
|
| 45 |
+
for face in (pose_data.get('faces') or []):
|
| 46 |
+
scan_points(face)
|
| 47 |
+
|
| 48 |
+
# 正規化(<=1)っぽい場合はNone返却
|
| 49 |
+
if max_x <= 1.01 and max_y <= 1.01:
|
| 50 |
+
return None
|
| 51 |
+
# ゼロは不正
|
| 52 |
+
if max_x <= 0 or max_y <= 0:
|
| 53 |
+
return None
|
| 54 |
+
# 端数をそのまま使うより、丸め込む(最小でも整数)
|
| 55 |
+
return (int(round(max_x)), int(round(max_y)))
|
| 56 |
+
except Exception:
|
| 57 |
+
return None
|
| 58 |
+
|
| 59 |
def get_timestamp_filename(prefix, extension):
|
| 60 |
"""
|
| 61 |
タイムスタンプ付きファイル名を生成
|
|
|
|
| 98 |
draw = ImageDraw.Draw(image)
|
| 99 |
print(f"[DEBUG] 🎨 背景画像作成完了: {canvas_size}")
|
| 100 |
|
| 101 |
+
# 解像度(元データ座標系)
|
| 102 |
+
src_w, src_h = canvas_size
|
| 103 |
+
if isinstance(pose_data, dict):
|
| 104 |
+
if 'resolution' in pose_data and isinstance(pose_data['resolution'], (list, tuple)) and len(pose_data['resolution']) >= 2:
|
| 105 |
+
src_w, src_h = int(pose_data['resolution'][0] or canvas_size[0]), int(pose_data['resolution'][1] or canvas_size[1])
|
| 106 |
+
elif 'metadata' in pose_data and isinstance(pose_data['metadata'], dict) and 'resolution' in pose_data['metadata']:
|
| 107 |
+
res = pose_data['metadata'].get('resolution', canvas_size)
|
| 108 |
+
if isinstance(res, (list, tuple)) and len(res) >= 2:
|
| 109 |
+
src_w, src_h = int(res[0] or canvas_size[0]), int(res[1] or canvas_size[1])
|
| 110 |
+
|
| 111 |
+
# 解像度が未設定のときのみ、データから推定した解像度を利用(誤検出による過度な拡大を防止)
|
| 112 |
+
if (not isinstance(pose_data, dict)) or (
|
| 113 |
+
('resolution' not in pose_data or not pose_data.get('resolution')) and
|
| 114 |
+
(pose_data.get('metadata') is None or not pose_data['metadata'].get('resolution'))
|
| 115 |
+
):
|
| 116 |
+
detected = _detect_source_resolution_from_data(pose_data)
|
| 117 |
+
if detected is not None:
|
| 118 |
+
src_w, src_h = detected
|
| 119 |
+
|
| 120 |
# ボディの描画(refs準拠)
|
| 121 |
+
print(f"[DEBUG] 🧭 Export scale info: src_res=({src_w},{src_h}) -> out=({canvas_size[0]},{canvas_size[1]})")
|
| 122 |
if 'people' in pose_data and pose_data['people']:
|
| 123 |
print(f"[DEBUG] 🎨 ボディ描画開始(refs準拠)")
|
| 124 |
+
draw_body_on_image(draw, pose_data, canvas_size, (src_w, src_h))
|
| 125 |
print(f"[DEBUG] 🎨 ボディ描画完了")
|
| 126 |
else:
|
| 127 |
print(f"[DEBUG] ⚠️ ボディデータなし - people: {'people' in pose_data}, count: {len(pose_data.get('people', []))}")
|
|
|
|
| 141 |
print(f"[DEBUG] 🎨 手描画開始(hands形式)")
|
| 142 |
|
| 143 |
if hands_data:
|
| 144 |
+
draw_hands_on_image(draw, hands_data, canvas_size, (src_w, src_h))
|
| 145 |
print(f"[DEBUG] 🎨 手描画完了")
|
| 146 |
else:
|
| 147 |
print(f"[DEBUG] ⚠️ 手描画スキップ - 手データなし")
|
|
|
|
| 160 |
print(f"[DEBUG] 🎨 顔描画開始(faces形式)")
|
| 161 |
|
| 162 |
if face_data:
|
| 163 |
+
draw_faces_on_image(draw, face_data, canvas_size, (src_w, src_h))
|
| 164 |
print(f"[DEBUG] 🎨 顔描画完了")
|
| 165 |
else:
|
| 166 |
print(f"[DEBUG] ⚠️ 顔描画スキップ - 顔データなし")
|
|
|
|
| 174 |
notify_error(f"ポーズ画像エクスポートに失敗しました: {str(e)}")
|
| 175 |
return None
|
| 176 |
|
| 177 |
+
def draw_body_on_image(draw, pose_data, canvas_size, source_resolution=None):
|
| 178 |
"""画像にボディを描画(refs準拠)"""
|
| 179 |
try:
|
| 180 |
print(f"[DEBUG] 🎨 draw_body_on_image開始(refs準拠)")
|
|
|
|
| 200 |
]
|
| 201 |
|
| 202 |
W, H = canvas_size
|
| 203 |
+
srcW, srcH = (source_resolution or canvas_size)
|
| 204 |
+
if srcW <= 0 or srcH <= 0:
|
| 205 |
+
srcW, srcH = W, H
|
| 206 |
detection_threshold = 0.3
|
| 207 |
|
| 208 |
for person in people:
|
|
|
|
| 218 |
|
| 219 |
print(f"[DEBUG] 🎨 keypoints count: {len(keypoints)}")
|
| 220 |
|
| 221 |
+
# 座標の正規化/解像度差吸収(0..1正規化 or ピクセル→出力解像度へスケール)
|
| 222 |
+
is_normalized = len(keypoints) > 0 and all(0 <= kp[0] <= 1 and 0 <= kp[1] <= 1 for kp in keypoints if kp[2] > 0)
|
| 223 |
+
if is_normalized:
|
| 224 |
for kp in keypoints:
|
| 225 |
if kp[2] > 0:
|
| 226 |
kp[0] *= W
|
| 227 |
kp[1] *= H
|
| 228 |
+
else:
|
| 229 |
+
# ピクセル座標 → 出力サイズへスケール(元解像度→出力解像度)
|
| 230 |
+
sx = W / float(srcW)
|
| 231 |
+
sy = H / float(srcH)
|
| 232 |
+
for kp in keypoints:
|
| 233 |
+
if kp[2] > 0:
|
| 234 |
+
kp[0] *= sx
|
| 235 |
+
kp[1] *= sy
|
| 236 |
|
| 237 |
# refs準拠:接続線の描画
|
| 238 |
for i, connection in enumerate(connections):
|
|
|
|
| 272 |
import traceback
|
| 273 |
traceback.print_exc()
|
| 274 |
|
| 275 |
+
def draw_hands_on_image(draw, hands_data, canvas_size, source_resolution=None):
|
| 276 |
"""💖 画像に手を描画(座標変換対応)"""
|
| 277 |
W, H = canvas_size
|
| 278 |
+
srcW, srcH = (source_resolution or canvas_size)
|
| 279 |
+
if srcW <= 0 or srcH <= 0:
|
| 280 |
+
srcW, srcH = W, H
|
| 281 |
for hand in hands_data:
|
| 282 |
if hand and len(hand) > 0:
|
| 283 |
for i in range(0, len(hand), 3):
|
| 284 |
if i + 2 < len(hand):
|
| 285 |
x, y, conf = hand[i], hand[i+1], hand[i+2]
|
| 286 |
if conf > 0.3:
|
| 287 |
+
# 💖 座標の正規化/ピクセルスケール
|
| 288 |
if 0 <= x <= 1 and 0 <= y <= 1:
|
| 289 |
x = x * W
|
| 290 |
y = y * H
|
| 291 |
+
else:
|
| 292 |
+
x = x * (W / float(srcW))
|
| 293 |
+
y = y * (H / float(srcH))
|
| 294 |
# refs準拠: OpenCV(255,0,0)BGR → PIL(0,0,255)RGB = 青
|
| 295 |
draw.ellipse([int(x)-3, int(y)-3, int(x)+3, int(y)+3], fill=(0, 0, 255))
|
| 296 |
|
| 297 |
+
def draw_faces_on_image(draw, faces_data, canvas_size, source_resolution=None):
|
| 298 |
"""💖 画像に顔を描画(座標変換対応)"""
|
| 299 |
W, H = canvas_size
|
| 300 |
+
srcW, srcH = (source_resolution or canvas_size)
|
| 301 |
+
if srcW <= 0 or srcH <= 0:
|
| 302 |
+
srcW, srcH = W, H
|
| 303 |
for face in faces_data:
|
| 304 |
if face and len(face) > 0:
|
| 305 |
for i in range(0, len(face), 3):
|
| 306 |
if i + 2 < len(face):
|
| 307 |
x, y, conf = face[i], face[i+1], face[i+2]
|
| 308 |
if conf > 0.3:
|
| 309 |
+
# 💖 座標の正規化/ピクセルスケール
|
| 310 |
if 0 <= x <= 1 and 0 <= y <= 1:
|
| 311 |
x = x * W
|
| 312 |
y = y * H
|
| 313 |
+
else:
|
| 314 |
+
x = x * (W / float(srcW))
|
| 315 |
+
y = y * (H / float(srcH))
|
| 316 |
# refs準拠: OpenCV(255,255,255)BGR → PIL(255,255,255)RGB = 白
|
| 317 |
draw.ellipse([int(x)-2, int(y)-2, int(x)+2, int(y)+2], fill=(255, 255, 255))
|
| 318 |
|
|
|
|
| 441 |
|
| 442 |
except Exception as e:
|
| 443 |
print(f"Download link creation error: {e}")
|
| 444 |
+
return None
|