Whyx-PROmpTea / scripts /validate_pose.py
ArtShumov's picture
fix: pose tag naming + swap DWPose detector to YOLOv8m-pose for anime
4fb75d4
Raw
History Blame Contribute Delete
9.91 kB
"""Validate pose detection on real anime/illustration images.
Runs both YOLOv8m-pose and DWPose wholebody estimators on images from a
directory and saves visualizations with keypoints overlaid + prints detected tags.
Usage:
python scripts/validate_pose.py <image_dir> [--limit N]
"""
import os
import sys
import argparse
import json
from pathlib import Path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import numpy as np
from PIL import Image, ImageDraw, ImageOps
# COCO-17 skeleton connections (for drawing pose lines)
_COCO_SKELETON = [
(0, 1), (0, 2), (1, 3), (2, 4), # head
(5, 6), (5, 7), (7, 9), # left arm
(6, 8), (8, 10), # right arm
(5, 11), (6, 12), (11, 12), # torso
(11, 13), (13, 15), # left leg
(12, 14), (14, 16), # right leg
]
_WB_HAND_SKELETON = [
(0, 1), (1, 2), (2, 3), (3, 4), (0, 5), (5, 6), (6, 7), (7, 8),
(0, 9), (9, 10), (10, 11), (11, 12), (0, 13), (13, 14), (14, 15), (15, 16),
(0, 17), (17, 18), (18, 19), (19, 20),
]
def _vis(kp: np.ndarray, i: int, thresh: float = 0.25) -> bool:
"""Check if keypoint i is visible."""
if kp.ndim != 2 or kp.shape[0] <= i:
return False
return kp[i, 2] >= thresh
def draw_keypoints(img: Image.Image, keypoints: np.ndarray, skeleton: list,
color=(255, 0, 0), radius=3, line_width=1) -> Image.Image:
"""Draw keypoints and skeleton lines on an image copy."""
canvas = img.convert("RGB")
draw = ImageDraw.Draw(canvas)
if keypoints.ndim == 2 and keypoints.shape[0] >= len(skeleton[0]) if skeleton else 17:
# Draw skeleton lines
for a, b in skeleton:
if _vis(keypoints, a) and _vis(keypoints, b):
draw.line([keypoints[a, 0], keypoints[a, 1],
keypoints[b, 0], keypoints[b, 1]],
fill=color, width=line_width)
# Draw keypoint dots with index labels
for i in range(keypoints.shape[0]):
if _vis(keypoints, i):
x, y = int(keypoints[i, 0]), int(keypoints[i, 1])
draw.ellipse([x - radius, y - radius, x + radius, y + radius],
fill=color)
draw.text((x + 4, y - 4), str(i), fill=(255, 255, 255))
return canvas
def draw_hands(img: Image.Image, keypoints: np.ndarray, hand_indices: list,
colors: list) -> Image.Image:
"""Draw hand skeletons on an image."""
draw = ImageDraw.Draw(img)
for hk, hand_color in zip(hand_indices, colors):
hand_kpts = keypoints[hk:hk + 21]
for a, b in _WB_HAND_SKELETON:
if _vis(hand_kpts, a, 0.15) and _vis(hand_kpts, b, 0.15):
draw.line([hand_kpts[a, 0], hand_kpts[a, 1],
hand_kpts[b, 0], hand_kpts[b, 1]],
fill=hand_color, width=1)
for i in range(21):
if _vis(hand_kpts, i, 0.15):
x, y = int(hand_kpts[i, 0]), int(hand_kpts[i, 1])
draw.ellipse([x - 2, y - 2, x + 2, y + 2], fill=hand_color)
# Draw face points (indices 23-90 in the 133-keypoint layout)
face_kpts = keypoints[23:91]
for i in range(face_kpts.shape[0]):
if _vis(face_kpts, i, 0.15):
x, y = int(face_kpts[i, 0]), int(face_kpts[i, 1])
draw.ellipse([x - 1, y - 1, x + 1, y + 1], fill=(255, 128, 0))
return img
def resize_for_display(img: Image.Image, max_size=800) -> Image.Image:
"""Resize image for display."""
w, h = img.size
if max(w, h) <= max_size:
return img
ratio = max_size / max(w, h)
return img.resize((int(w * ratio), int(h * ratio)), Image.LANCZOS)
def validate_on_image(img_path: str, output_dir: str) -> dict:
"""Run both pose estimators on a single image and save results + visualizations."""
result = {
"image": os.path.basename(img_path),
"yolo": {"detected": False, "people_count": 0, "pose_score": 0.0,
"pose_tags": [], "keypoints_count": 0},
"wholebody": {"detected": False, "people_count": 0, "pose_score": 0.0,
"pose_tags": [], "body_kpts": 0, "face_kpts": 0, "hand_kpts": 0},
"visualized": False,
}
try:
img = Image.open(img_path).convert("RGB")
img_exif = ImageOps.exif_transpose(img)
except Exception as e:
result["error"] = f"Could not load image: {e}"
return result
# --- Run YOLO pose (17 keypoints) ---
try:
from src.pose_tagger import get_pose_tagger
est = get_pose_tagger()
if est.ensure_loaded():
yolo_result = est.estimate(img_exif)
result["yolo"] = {
"detected": yolo_result.get("people_count", 0) > 0,
"people_count": yolo_result.get("people_count", 0),
"pose_score": float(yolo_result.get("pose_score", 0.0)),
"pose_tags": yolo_result.get("pose_tags", []),
"keypoints_count": len(yolo_result.get("keypoints", [])),
}
if yolo_result.get("keypoints"):
kpts_list = yolo_result["keypoints"]
if kpts_list and isinstance(kpts_list[0], (list, np.ndarray)):
kpts = np.array(kpts_list[0])
if kpts.ndim == 2 and kpts.shape[0] >= 17:
vis_img = draw_keypoints(
resize_for_display(img_exif), kpts, _COCO_SKELETON)
vis_img.save(os.path.join(
output_dir, f"yolo_{os.path.basename(img_path)}"))
else:
result["yolo"]["error"] = "YOLO model failed to load"
except Exception as e:
result["yolo"]["error"] = str(e)
# --- Run DWPose wholebody (133 keypoints) ---
try:
from src.wholebody_pose import get_wholebody_tagger
wb_est = get_wholebody_tagger()
if wb_est.ensure_loaded():
wb_result = wb_est.estimate(img_exif)
result["wholebody"] = {
"detected": wb_result.get("people_count", 0) > 0,
"people_count": wb_result.get("people_count", 0),
"pose_score": float(wb_result.get("pose_score", 0.0)),
"pose_tags": wb_result.get("pose_tags", []),
"body_kpts": len(wb_result.get("body_kpts", [])),
"face_kpts": len(wb_result.get("face_kpts", [])),
"hand_kpts": len(wb_result.get("hand_kpts", [])),
}
if wb_result.get("keypoints"):
kpts = np.array(wb_result["keypoints"])
if kpts.ndim == 2 and kpts.shape[0] >= 133:
# Draw body skeleton in green
vis_img = draw_keypoints(
resize_for_display(img_exif), kpts[:17],
_COCO_SKELETON, color=(0, 255, 0), radius=4)
# Draw hand skeletons + face points
vis_img = draw_hands(vis_img, kpts, [91, 112],
[(255, 0, 0), (0, 0, 255)])
vis_img.save(os.path.join(
output_dir, f"wb_{os.path.basename(img_path)}"))
result["visualized"] = True
else:
result["wholebody"]["error"] = "DWPose model failed to load"
except Exception as e:
result["wholebody"]["error"] = str(e)
return result
def main():
parser = argparse.ArgumentParser(
description="Validate pose detection on real images")
parser.add_argument("image_dir", help="Directory of images to validate")
parser.add_argument("--limit", type=int, default=20,
help="Max images to process")
parser.add_argument("--output", default="scripts/validation_output",
help="Output directory for visualizations")
args = parser.parse_args()
os.makedirs(args.output, exist_ok=True)
# Find images (skip Neg_ prefixed negative samples)
image_exts = {".jpg", ".jpeg", ".png", ".webp"}
images = sorted([
str(p) for p in Path(args.image_dir).iterdir()
if p.suffix.lower() in image_exts and not p.name.startswith("Neg_")
])[:args.limit]
if not images:
print("No images found!")
return
print(f"Found {len(images)} images to validate")
print(f"Output directory: {args.output}")
print()
results = []
for i, img_path in enumerate(images):
short_name = os.path.basename(img_path)
print(f"[{i + 1}/{len(images)}] {short_name}")
result = validate_on_image(img_path, args.output)
results.append(result)
# Print summary
y = result["yolo"]
w = result["wholebody"]
yolo_err = y.get("error", "")
wb_err = w.get("error", "")
print(f" YOLO: detected={y['detected']}, "
f"people={y['people_count']}, "
f"score={y['pose_score']:.3f}, "
f"tags={y['pose_tags']}")
if yolo_err:
print(f" YOLO ERR: {yolo_err}")
print(f" WB: detected={w['detected']}, "
f"people={w['people_count']}, "
f"score={w['pose_score']:.3f}, "
f"body_kpts={w['body_kpts']}, "
f"face_kpts={w['face_kpts']}, "
f"hand_kpts={w['hand_kpts']}")
print(f" WB tags: {w['pose_tags']}")
if wb_err:
print(f" WB ERR: {wb_err}")
print()
# Save results summary
summary_path = os.path.join(args.output, "validation_summary.json")
with open(summary_path, "w", encoding="utf-8") as f:
json.dump(results, f, indent=2, ensure_ascii=False)
print(f"Results saved to {summary_path}")
if __name__ == "__main__":
main()