grmchn commited on
Commit
ac4b077
·
1 Parent(s): 521c583

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
Files changed (3) hide show
  1. app.py +105 -16
  2. static/pose_editor.js +497 -183
  3. 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
- return pose_result, pose_result, gr.update()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 people_format_data, people_format_data
 
 
 
 
 
 
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
- return display_data, display_data
 
 
 
 
 
 
 
 
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
- return hybrid_data, hybrid_data
 
 
 
 
 
 
 
 
 
 
 
 
 
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: true,
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 scaleX = canvas.width / originalRes[0];
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] * scaleX;
316
- const scaledY = point[1] * scaleY;
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 scaleX = canvas.width / originalRes[0];
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] * scaleX;
359
- const y = handData[i + 1] * scaleY;
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] * scaleX;
392
- const y = faceData[i + 1] * scaleY;
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 scaledX = point[0] * scaleX;
418
- const scaledY = point[1] * scaleY;
 
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 scaleX = canvas.width / resolution[0];
453
- const scaleY = canvas.height / resolution[1];
454
- dataClickX = clickX / scaleX;
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 scaleX = canvas.width / originalRes[0];
720
- const scaleY = canvas.height / originalRes[1];
721
 
722
  const point = candidates[keypointIndex];
723
  if (point) {
724
- const keypointX = point[0] * scaleX;
725
- const keypointY = point[1] * scaleY;
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
- const scaleX = canvas.width / originalRes[0];
751
- const scaleY = canvas.height / originalRes[1];
752
-
753
- // Canvas座標をデータ座標に変換
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
- const coordScaleX = canvasWidth / dataResolutionWidth;
958
- const coordScaleY = canvasHeight / dataResolutionHeight;
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
- canvasX = (x * dataResolutionWidth) * coordScaleX;
986
- canvasY = (y * dataResolutionHeight) * coordScaleY;
 
 
987
  } else {
988
- canvasX = x * coordScaleX;
989
- canvasY = y * coordScaleY;
990
  }
991
 
992
  // 元矩形内での相対位置を計算
@@ -999,13 +999,13 @@ function transformKeypointsInRect(control, newMouseX, newMouseY) {
999
 
1000
  // Canvas座標→データ座標に戻す
1001
  if (isNormalized) {
1002
- const dataX = newCanvasX / coordScaleX;
1003
- const dataY = newCanvasY / coordScaleY;
1004
  targetKeypoints[i] = dataX / dataResolutionWidth;
1005
  targetKeypoints[i + 1] = dataY / dataResolutionHeight;
1006
  } else {
1007
- targetKeypoints[i] = newCanvasX / coordScaleX;
1008
- targetKeypoints[i + 1] = newCanvasY / coordScaleY;
1009
  }
1010
  }
1011
  }
@@ -1080,8 +1080,7 @@ function transformKeypointsDirectly(rectType, originalRect, newRect) {
1080
  dataResolutionHeight = currentPoseData.resolution[1];
1081
  }
1082
 
1083
- const coordScaleX = canvasWidth / dataResolutionWidth;
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
- coordScale: { x: coordScaleX, y: coordScaleY },
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) * coordScaleX;
1142
- cy = (y * dataResolutionHeight) * coordScaleY;
1143
  } else if (isCanvasUnit) {
1144
  cx = x; cy = y;
1145
  } else {
1146
- cx = x * coordScaleX;
1147
- cy = y * coordScaleY;
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) * coordScaleX;
1179
- canvasY = (y * dataResolutionHeight) * coordScaleY;
1180
  } else if (isCanvasUnit) {
1181
  // 既にCanvas座標(過去のバグで混入している場合)
1182
  canvasX = x;
1183
  canvasY = y;
1184
  } else {
1185
- canvasX = x * coordScaleX;
1186
- canvasY = y * coordScaleY;
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 / coordScaleX;
1204
- const dataY = newCanvasY / coordScaleY;
1205
  finalX = dataX / dataResolutionWidth;
1206
  finalY = dataY / dataResolutionHeight;
1207
  targetKeypoints[i] = finalX;
1208
  targetKeypoints[i + 1] = finalY;
1209
  } else {
1210
- finalX = newCanvasX / coordScaleX;
1211
- finalY = newCanvasY / coordScaleY;
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 coordScaleX = canvasWidth / dataResolutionWidth;
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) * coordScaleX;
1359
- canvasY = (origY * dataResolutionHeight) * coordScaleY;
1360
  } else {
1361
- canvasX = origX * coordScaleX;
1362
- canvasY = origY * coordScaleY;
1363
  }
1364
 
1365
  // 移動量を適用
@@ -1368,13 +1366,13 @@ function moveKeypointsFromOriginal(rectType, totalDeltaX, totalDeltaY) {
1368
 
1369
  // Canvas座標→データ座標に戻す
1370
  if (isNormalized) {
1371
- const dataX = newCanvasX / coordScaleX;
1372
- const dataY = newCanvasY / coordScaleY;
1373
  targetKeypoints[i] = dataX / dataResolutionWidth;
1374
  targetKeypoints[i + 1] = dataY / dataResolutionHeight;
1375
  } else {
1376
- targetKeypoints[i] = newCanvasX / coordScaleX;
1377
- targetKeypoints[i + 1] = newCanvasY / coordScaleY;
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
- const coordScaleX = canvasWidth / dataResolutionWidth;
1433
- const coordScaleY = canvasHeight / dataResolutionHeight;
1434
 
1435
  // Canvasの移動量→データ座標の移動量へ変換
1436
- const dataDeltaX = deltaX / coordScaleX;
1437
- const dataDeltaY = deltaY / coordScaleY;
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
- const scaleX = canvas.width / originalRes[0];
1613
- const scaleY = canvas.height / originalRes[1];
1614
-
1615
- // 🔧 refs互換:Canvas座標をデータ座標に変換してからクランプ
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 scaleX = canvas.width / originalRes[0];
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, scaleX, scaleY);
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, scaleX, scaleY);
1966
  } else {
1967
  // 💖 people形式のみサポート、空データで矩形なし
1968
- drawHandRectangles([[], []], originalRes, scaleX, scaleY);
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, scaleX, scaleY);
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, scaleX, scaleY);
2008
  } else {
2009
- drawFaceRectangles(currentPoseData.faces, originalRes, scaleX, scaleY);
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 scaleX = canvas.width / originalRes[0];
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] * scaleX;
2123
- const startY = startPoint[1] * scaleY;
2124
- const endX = endPoint[0] * scaleX;
2125
- const endY = endPoint[1] * scaleY;
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] * scaleX;
2153
- const scaledY = point[1] * scaleY;
2154
 
2155
  // 🔧 ハイライト対応: ドラッグ中のキーポイントを強調表示
2156
  if (i === highlightIndex) {
@@ -2175,8 +2159,8 @@ function drawBody(poseData, highlightIndex = -1) {
2175
 
2176
  // 🎨 補間機能: 有効キーポイントが少ない場合の視覚的改善
2177
  if (drawnKeypoints < 10) {
2178
- // console.log(`[DEBUG] 💡 Low keypoint count (${drawnKeypoints}), applying visual enhancements`);
2179
- drawEstimatedConnections(candidates, originalRes, scaleX, scaleY);
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, scaleX, scaleY) {
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 scaledX = x * scaleX;
2224
- const scaledY = y * scaleY;
 
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, scaleX, scaleY) {
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 scaledX = x * scaleX;
2302
- const scaledY = y * scaleY;
 
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 = transformCoordinate(x, y, dataRes[0], dataRes[1]);
2383
  drawKeypoint(scaled.x, scaled.y, radius);
2384
  }
2385
 
2386
  // Canvas解像度更新
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2387
  function updateCanvasResolution(width, height) {
2388
  if (!canvas) return false;
2389
-
2390
- canvas.width = width;
2391
- canvas.height = height;
2392
-
2393
- coordinateTransformer.updateResolution(null, [width, height]);
2394
-
2395
- // 現在のポーズデータを再描画
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2396
  if (poseData) {
2397
- drawPose(poseData);
 
 
2398
  }
2399
-
2400
- notifyCanvasOperation(`Canvas解像度を${width}x${height}に変更しました`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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, scaleX, scaleY) {
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, scaleX, scaleY);
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, scaleX, scaleY) {
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, scaleX, scaleY);
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
- // drawHands関数では scaledX = x * scaleX しているため、
2779
- // 矩形計算でも同じ変換を適用して座標系を統一
2780
- const finalX = x * scaleX;
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 scaledX = x * scaleX;
2865
- const scaledY = y * scaleY;
 
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 scaleX = canvas.width / originalRes[0];
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, scaleX, scaleY);
2978
  }
2979
 
2980
  // 顔の描画(設定制御・座標変換パラメータ付き)
2981
  if (window.poseEditorGlobals.enableFace && currentPoseData.faces) {
2982
- drawFaces(currentPoseData.faces, originalRes, scaleX, scaleY);
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 scaleX = 640 / 512; // 1.25
3355
- const scaleY = 640 / 512; // 1.25
3356
-
3357
- const origRectDataX = originalRect.x / scaleX;
3358
- const origRectDataY = originalRect.y / scaleY;
3359
- const newRectDataX = newRect.x / scaleX;
3360
- const newRectDataY = newRect.y / scaleY;
3361
- const origRectWidthData = originalRect.width / scaleX;
3362
- const origRectHeightData = originalRect.height / scaleY;
3363
- const newRectWidthData = newRect.width / scaleX;
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 scaleX = 640 / 512; // 1.25
3415
- const scaleY = 640 / 512; // 1.25
3416
- const dataDeltaX = deltaX / scaleX;
3417
- const dataDeltaY = deltaY / scaleY;
3418
 
3419
  // 移動(512x512にクランプ)
3420
- const newX = Math.max(0, Math.min(512, currentX + dataDeltaX));
3421
- const newY = Math.max(0, Math.min(512, currentY + dataDeltaY));
 
 
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 coordScaleX = canvas.width / dataResolutionWidth;
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) * coordScaleX;
3592
- canvasY = (y * dataResolutionHeight) * coordScaleY;
3593
  } else {
3594
- canvasX = x * coordScaleX;
3595
- canvasY = y * coordScaleY;
3596
  }
3597
 
3598
  // 🔧 元矩形内での相対位置を計算(refs互換・安全範囲チェック)
@@ -3609,8 +3859,8 @@ function updateKeypointsByRect(rectType, newRect) {
3609
 
3610
  // 🔧 Canvas座標→データ座標に戻す(refs互換・範囲制限付き)
3611
  if (isNormalized) {
3612
- const dataX = newCanvasX / coordScaleX;
3613
- const dataY = newCanvasY / coordScaleY;
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 / coordScaleX;
3625
- let newDataY = newCanvasY / coordScaleY;
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 coordScaleX = canvas.width / dataResolutionWidth;
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 / coordScaleX;
3727
- const dataDeltaY = deltaY / coordScaleY;
3728
 
3729
 
3730
  let movedCount = 0;
@@ -3751,8 +4000,9 @@ function moveKeypointsByRect(rectType, deltaX, deltaY) {
3751
  }
3752
 
3753
  // 🎨 推定接続の描画(少ないキーポイント用の補間機能)
3754
- function drawEstimatedConnections(candidates, originalRes, scaleX, scaleY) {
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] * scaleX,
3766
- y: point[1] * scaleY,
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
- # 座標の正規化チェックと変換(refs準拠)
151
- if len(keypoints) > 0 and all(0 <= kp[0] <= 1 and 0 <= kp[1] <= 1 for kp in keypoints if kp[2] > 0):
 
152
  for kp in keypoints:
153
  if kp[2] > 0:
154
  kp[0] *= W
155
  kp[1] *= H
 
 
 
 
 
 
 
 
156
 
157
  # refs準拠:接続線の描画
158
  for i, connection in enumerate(connections):
@@ -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
- # 💖 座標の正規化チェック(0-1の場合はCanvas座標に変換)
205
  if 0 <= x <= 1 and 0 <= y <= 1:
206
  x = x * W
207
  y = y * H
 
 
 
208
  # refs準拠: OpenCV(255,0,0)BGR → PIL(0,0,255)RGB = 青
209
  draw.ellipse([int(x)-3, int(y)-3, int(x)+3, int(y)+3], fill=(0, 0, 255))
210
 
211
- def draw_faces_on_image(draw, faces_data, canvas_size):
212
  """💖 画像に顔を描画(座標変換対応)"""
213
  W, H = canvas_size
 
 
 
214
  for face in faces_data:
215
  if face and len(face) > 0:
216
  for i in range(0, len(face), 3):
217
  if i + 2 < len(face):
218
  x, y, conf = face[i], face[i+1], face[i+2]
219
  if conf > 0.3:
220
- # 💖 座標の正規化チェック(0-1の場合はCanvas座標に変換)
221
  if 0 <= x <= 1 and 0 <= y <= 1:
222
  x = x * W
223
  y = y * H
 
 
 
224
  # refs準拠: OpenCV(255,255,255)BGR → PIL(255,255,255)RGB = 白
225
  draw.ellipse([int(x)-2, int(y)-2, int(x)+2, int(y)+2], fill=(255, 255, 255))
226
 
@@ -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