Files
by-aeandCursor Agent 00dcbc0edd Fix flake8 linting errors
Fix undefined name errors in image_selector.py by initializing button
position variables (select_all_x, sort_mode_x, sort_dir_x) before the
event loop. These variables are recalculated each frame but need to be
defined before first use in the event handler.

Fix unused nonlocal warning in pose_editor.py by removing unnecessary
'nonlocal undo_buffer' declaration. The buffer is only modified in-place,
not reassigned, so nonlocal is not needed.

These were pre-existing bugs that prevented CI from passing.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-10-22 22:08:26 +00:00

1426 lines
68 KiB
Python

import pygame
import torch
import numpy as np
import math
import hashlib
import json
import os
import subprocess
import re
import platform
from pygame import surfarray
# pygame.init() # Moved inside pose_editor function
# Cache file location (initialized lazily)
import os
CACHE_DIR = None
CACHE_FILE = None
WINDOW_CACHE_FILE = None
def get_cache_dir():
"""Get cache directory, initializing if needed"""
global CACHE_DIR, CACHE_FILE, WINDOW_CACHE_FILE
if CACHE_DIR is None:
try:
from folder_paths import get_user_directory
CACHE_DIR = os.path.join(get_user_directory(), "ae-in-workflow", "pose_editor")
except ImportError:
# Fallback if folder_paths not available
CACHE_DIR = os.path.expanduser("~/.comfyui_cache/ae-in-workflow/pose_editor")
CACHE_FILE = os.path.join(CACHE_DIR, "cache.json")
WINDOW_CACHE_FILE = os.path.join(CACHE_DIR, "window_position.json")
# Ensure cache directory exists
os.makedirs(CACHE_DIR, exist_ok=True)
return CACHE_DIR
# Undo/Redo constants
MAX_UNDO_STEPS = 5000 # Maximum undo steps to keep
# Common pose structure (same for all people)
# NOTE: Unsure if this is the correct "official order" as there was a lot of contradicting info on pose layouts
# From what I gather the colors, order, etc. are essentially varioua, but this one mostly aligns with cnet preprocessor colors
POSE_PAIRS = [
(1, 0), (0, 14), (14, 16), (0, 15), (15, 17),
(1, 2), (2, 3), (3, 4),
(1, 5), (5, 6), (6, 7),
(1, 8), (8, 9), (9, 10),
(1, 11), (11, 12), (12, 13)
]
POSE_COLORS = [
(0, 0, 152), # (1, 0) neck → nose
(54, 0, 152), # (0, 14) nose → right eye
(102, 0, 152), # (14, 16) right eye → right ear
(152, 0, 152), # (0, 15) nose → left eye
(152, 0, 102), # (15, 17) left eye → left ear
(152, 0, 0), # (1, 2) neck → right shoulder
(152, 102, 0), # (2, 3) right shoulder → right elbow
(152, 152, 0), # (3, 4) right elbow → right wrist
(152, 54, 0), # (1, 5) neck → left shoulder
(102, 152, 0), # (5, 6) left shoulder → left elbow
(54, 152, 0), # (6, 7) left elbow → left wrist
(0, 152, 0), # (1, 8) neck → right hip
(0, 152, 54), # (8, 9) right hip → right knee
(0, 152, 102), # (9, 10) right knee → right ankle
(0, 152, 152), # (1, 11) neck → left hip
(0, 102, 152), # (11, 12) left hip → left knee
(0, 54, 152), # (12, 13) left knee → left ankle
(255, 0, 85)
]
def get_child_keypoints(parent_idx):
"""Get all direct child keypoints of a given keypoint index"""
children = []
for parent, child in POSE_PAIRS:
if parent == parent_idx:
children.append(child)
return children
def get_all_descendants(root_idx):
"""Get all descendants (children, grandchildren, etc.) of a keypoint recursively"""
descendants = set()
to_process = [root_idx]
while to_process:
current = to_process.pop()
children = get_child_keypoints(current)
for child in children:
if child not in descendants:
descendants.add(child)
to_process.append(child)
return descendants
def get_nearest_bone(mouse_pos, people_keypoints, people_valid_keypoints, person_idx):
"""Find the bone (pair of keypoints) closest to the mouse position for the given person"""
mouse_x, mouse_y = mouse_pos
current_keypoints = people_keypoints[person_idx]
valid_keypoints = people_valid_keypoints[person_idx]
min_distance = float('inf')
nearest_bone = None
# Check all valid bone pairs
for i, j in POSE_PAIRS:
if i in valid_keypoints and j in valid_keypoints:
p1 = current_keypoints[i]
p2 = current_keypoints[j]
# Calculate distance from mouse to line segment
# Using distance to line formula
dx = p2[0] - p1[0]
dy = p2[1] - p1[1]
if dx == 0 and dy == 0:
# Points are the same, use distance to point
distance = ((p1[0] - mouse_x)**2 + (p1[1] - mouse_y)**2)**0.5
else:
# Distance to line segment
t = ((mouse_x - p1[0]) * dx + (mouse_y - p1[1]) * dy) / (dx*dx + dy*dy)
t = max(0, min(1, t)) # Clamp to segment
closest_x = p1[0] + t * dx
closest_y = p1[1] + t * dy
distance = ((closest_x - mouse_x)**2 + (closest_y - mouse_y)**2)**0.5
if distance < min_distance:
min_distance = distance
nearest_bone = (i, j)
return nearest_bone
def get_descendants_with_neck_special_case(root_idx):
"""Get descendants with special case for neck - only head chain, no arms/legs"""
if root_idx == 1: # Neck
# For neck, only return head-related descendants (nose, eyes, ears)
# Exclude shoulders (2, 5) and hips (8, 11) and their descendants
descendants = get_all_descendants(root_idx)
# Remove arm and leg chains
arm_leg_keypoints = {2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13} # All arm and leg keypoints
return descendants - arm_leg_keypoints
else:
return get_all_descendants(root_idx)
def flip_person_horizontal(person_idx, people_keypoints, people_valid_keypoints, swap_left_right=True):
"""Flip a person horizontally around their vertical center line"""
keypoints = people_keypoints[person_idx]
# Find the vertical center line (average of shoulder positions or neck)
if 1 in people_valid_keypoints[person_idx]: # neck
center_x = keypoints[1][0]
elif 2 in people_valid_keypoints[person_idx] and 5 in people_valid_keypoints[person_idx]: # shoulders
center_x = (keypoints[2][0] + keypoints[5][0]) / 2
else:
# Fallback: use average of all valid keypoints
valid_points = [keypoints[j] for j in people_valid_keypoints[person_idx]]
if valid_points:
center_x = sum(p[0] for p in valid_points) / len(valid_points)
else:
return # No valid keypoints to flip
# Define left-right keypoint pairs to swap
lr_pairs = [
(2, 5), # shoulders
(3, 6), # elbows
(4, 7), # wrists
(8, 11), # hips
(9, 12), # knees
(10, 13), # ankles
(14, 15), # eyes
(16, 17), # ears
]
# Flip coordinates horizontally
for i in range(len(keypoints)):
if i in people_valid_keypoints[person_idx]:
# Reflect over center line
keypoints[i][0] = 2 * center_x - keypoints[i][0]
# Swap left-right keypoints if requested
if swap_left_right:
for left_idx, right_idx in lr_pairs:
if (left_idx in people_valid_keypoints[person_idx] and
right_idx in people_valid_keypoints[person_idx]):
# Swap the keypoints
keypoints[left_idx], keypoints[right_idx] = keypoints[right_idx], keypoints[left_idx]
def get_pose_hash(pose_data):
"""Generate a hash for the pose data to use as cache key"""
pose_str = json.dumps(pose_data, sort_keys=True)
return hashlib.md5(pose_str.encode()).hexdigest()
def get_window_position_fallback(window_title):
"""Get window position using platform-specific system tools"""
system = platform.system()
if system == 'Linux':
# Linux/X11: use xwininfo
try:
result = subprocess.run(['xwininfo', '-name', window_title],
capture_output=True, text=True, timeout=2)
if result.returncode == 0:
output = result.stdout
abs_x_match = re.search(r'Absolute upper-left X:\s*(\d+)', output)
abs_y_match = re.search(r'Absolute upper-left Y:\s*(\d+)', output)
if abs_x_match and abs_y_match:
return int(abs_x_match.group(1)), int(abs_y_match.group(1))
except (subprocess.TimeoutExpired, FileNotFoundError, subprocess.SubprocessError):
pass
elif system == 'Windows':
# Windows: try PowerShell with Win32 API
try:
ps_command = f'''
$window = Get-Process | Where-Object {{$_.MainWindowTitle -eq "{window_title}"}} | Select-Object -First 1
if ($window) {{
$handle = $window.MainWindowHandle
Add-Type @"
using System;
using System.Runtime.InteropServices;
public class Win32 {{
[DllImport("user32.dll")]
public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
[StructLayout(LayoutKind.Sequential)]
public struct RECT {{ public int Left; public int Top; public int Right; public int Bottom; }}
}}
"@
$rect = New-Object Win32+RECT
[Win32]::GetWindowRect($handle, [ref]$rect)
Write-Output "$($rect.Left),$($rect.Top)"
}}
'''
result = subprocess.run(['powershell', '-Command', ps_command],
capture_output=True, text=True, timeout=5)
if result.returncode == 0 and result.stdout.strip():
parts = result.stdout.strip().split(',')
if len(parts) == 2:
return int(parts[0]), int(parts[1])
except (subprocess.TimeoutExpired, FileNotFoundError, subprocess.SubprocessError):
pass
elif system == 'Darwin': # macOS
# macOS: try osascript with System Events
try:
script = f'''
tell application "System Events"
set procList to (every process whose name is "Python")
if (count of procList) > 0 then
set frontProc to item 1 of procList
tell frontProc
set windowList to (every window whose name contains "{window_title}")
if (count of windowList) > 0 then
set frontWindow to item 1 of windowList
set windowBounds to bounds of frontWindow
return windowBounds
end if
end tell
end if
end tell
'''
result = subprocess.run(['osascript', '-e', script],
capture_output=True, text=True, timeout=5)
if result.returncode == 0 and result.stdout.strip():
# osascript returns bounds as: left,top,right,bottom
bounds = result.stdout.strip().split(',')
if len(bounds) == 4:
return int(bounds[0]), int(bounds[1])
except (subprocess.TimeoutExpired, FileNotFoundError, subprocess.SubprocessError):
pass
return None
def create_t_pose():
"""Create a natural-looking T-pose template based on human proportions"""
# Natural T-pose keypoints (x, y) coordinates
# Based on typical human proportions: head ~1/8, torso ~1/3, etc.
center_x = 400
base_y = 180 # Started higher for more natural head position
# TODO: extract a good default average human t-pose to use as a template
# This one is more than fine for now though.
t_pose_keypoints = [
[center_x, base_y], # 0: Nose
[center_x, base_y + 25], # 1: Neck
[center_x - 40, base_y + 30], # 2: Right shoulder (slightly lower than neck)
[center_x - 90, base_y + 45], # 3: Right elbow (angled slightly down)
[center_x - 140, base_y + 50], # 4: Right wrist
[center_x + 40, base_y + 30], # 5: Left shoulder
[center_x + 90, base_y + 45], # 6: Left elbow
[center_x + 140, base_y + 50], # 7: Left wrist
[center_x - 25, base_y + 120], # 8: Right hip
[center_x - 25, base_y + 220], # 9: Right knee
[center_x - 25, base_y + 320], # 10: Right ankle
[center_x + 25, base_y + 120], # 11: Left hip
[center_x + 25, base_y + 220], # 12: Left knee
[center_x + 25, base_y + 320], # 13: Left ankle
[center_x - 15, base_y - 15], # 14: Right eye
[center_x + 15, base_y - 15], # 15: Left eye
[center_x - 30, base_y - 10], # 16: Right ear
[center_x + 30, base_y - 10], # 17: Left ear
]
return t_pose_keypoints
def fix_missing_keypoints(person_idx, people_keypoints, people_valid_keypoints, people_valid_pairs, people_valid_colors, people_original_confidences):
"""Fix missing keypoints on a person using scaled T-pose proportions"""
t_pose = create_t_pose()
person_keypoints = people_keypoints[person_idx]
valid_keypoints = people_valid_keypoints[person_idx]
# Find neck position (keypoint 1)
if 1 not in valid_keypoints:
return 0 # Can't fix without neck reference
neck_x, neck_y = person_keypoints[1]
t_pose_neck_x, t_pose_neck_y = t_pose[1]
# Calculate scale factor based on existing keypoints
scale_factors = []
for i in valid_keypoints:
if i < len(t_pose) and i != 1: # Skip neck itself
# Distance from neck in person's pose
person_dx = person_keypoints[i][0] - neck_x
person_dy = person_keypoints[i][1] - neck_y
person_dist = math.sqrt(person_dx*person_dx + person_dy*person_dy)
# Distance from neck in T-pose
t_pose_dx = t_pose[i][0] - t_pose_neck_x
t_pose_dy = t_pose[i][1] - t_pose_neck_y
t_pose_dist = math.sqrt(t_pose_dx*t_pose_dx + t_pose_dy*t_pose_dy)
if t_pose_dist > 0: # Avoid division by zero
scale_factors.append(person_dist / t_pose_dist)
# Use median scale factor to be robust against outliers
if scale_factors:
scale_factors.sort()
scale_factor = scale_factors[len(scale_factors) // 2] # median
else:
scale_factor = 1.0 # fallback if no valid measurements
print(f"Person {person_idx}: using scale factor {scale_factor:.2f} (from {len(scale_factors)} reference points)")
fixed_count = 0
# For each keypoint that's missing
for i in range(len(person_keypoints)):
if i not in valid_keypoints and i < len(t_pose):
# Calculate scaled position relative to neck in T-pose
t_pose_keypoint_x, t_pose_keypoint_y = t_pose[i]
relative_x = (t_pose_keypoint_x - t_pose_neck_x) * scale_factor
relative_y = (t_pose_keypoint_y - t_pose_neck_y) * scale_factor
# Apply the scaled relative position to the person's neck
person_keypoints[i] = [neck_x + relative_x, neck_y + relative_y]
# Add to valid keypoints
valid_keypoints.append(i)
# Update confidence to make this keypoint visible
if person_idx < len(people_original_confidences) and i < len(people_original_confidences[person_idx]):
people_original_confidences[person_idx][i] = 1.0
fixed_count += 1
# Re-filter pairs and colors for this person to include new keypoints
if fixed_count > 0:
valid_pairs = [(i, j) for (i, j), col in zip(POSE_PAIRS, POSE_COLORS) if i in valid_keypoints and j in valid_keypoints]
valid_colors = [col for (i, j), col in zip(POSE_PAIRS, POSE_COLORS) if i in valid_keypoints and j in valid_keypoints]
people_valid_pairs[person_idx] = valid_pairs
people_valid_colors[person_idx] = valid_colors
return fixed_count
def space_out_people(people_keypoints, people_valid_keypoints):
"""Space out people who are at the same center location"""
if len(people_keypoints) <= 1:
return people_keypoints
# Calculate centers for each person
centers = []
for i, keypoints in enumerate(people_keypoints):
valid_points = [keypoints[j] for j in people_valid_keypoints[i]]
if valid_points:
cx = sum(p[0] for p in valid_points) / len(valid_points)
cy = sum(p[1] for p in valid_points) / len(valid_points)
centers.append((cx, cy, i)) # (x, y, person_index)
else:
centers.append((0, 0, i)) # fallback
# Group people by center location (with some tolerance)
tolerance = 50.0 # pixels
groups = {}
for cx, cy, idx in centers:
found_group = False
for group_center in groups:
if abs(cx - group_center[0]) < tolerance and abs(cy - group_center[1]) < tolerance:
groups[group_center].append(idx)
found_group = True
break
if not found_group:
groups[(cx, cy)] = [idx]
# Space out groups with multiple people
spaced_keypoints = people_keypoints.copy()
for group_center, person_indices in groups.items():
if len(person_indices) > 1:
# Calculate spacing
spacing = 200.0 # pixels between people
total_width = (len(person_indices) - 1) * spacing
start_x = group_center[0] - total_width / 2
# Check if we need to scale down to fit screen
info = pygame.display.Info()
screen_width = info.current_w
scale_factor = 1.0
if total_width > screen_width * 0.8:
scale_factor = (screen_width * 0.8) / total_width
# Only move the highest-indexed person (newest addition) in each group
# Leave existing people at their positions
max_idx = max(person_indices)
for i, person_idx in enumerate(person_indices):
if person_idx == max_idx:
# This is the newest person - move it to a spaced position
offset_x = (start_x + i * spacing * scale_factor) - group_center[0]
# Apply offset and scaling to all keypoints of this person
for j, point in enumerate(spaced_keypoints[person_idx]):
dx = point[0] - group_center[0]
dy = point[1] - group_center[1]
spaced_keypoints[person_idx][j][0] = group_center[0] + dx * scale_factor + offset_x
spaced_keypoints[person_idx][j][1] = group_center[1] + dy * scale_factor
# Other people in the group stay at their current positions
return spaced_keypoints
def get_closest_person(mouse_pos, people_keypoints, people_valid_keypoints):
"""Find the person with the closest visible keypoint to the mouse position"""
if not people_keypoints:
return 0
mouse_x, mouse_y = mouse_pos
closest_person = 0
min_distance = float('inf')
for i, keypoints in enumerate(people_keypoints):
valid_keypoint_indices = people_valid_keypoints[i]
for j in valid_keypoint_indices:
px, py = keypoints[j]
distance = (px - mouse_x) ** 2 + (py - mouse_y) ** 2
if distance < min_distance:
min_distance = distance
closest_person = i
return closest_person
def draw_rotated_ellipse(surface, color, center, width, height, angle):
"""Draw a rotated ellipse using OpenPose-style approach"""
# NOTE: I eyeballed this, it *seemed* like they were using rotated narrow ellipses
# Seems to look and work fine so YOLO
# Create a surface for the ellipse
ellipse_surface = pygame.Surface((width, height), pygame.SRCALPHA)
# Draw ellipse on the surface (filled)
pygame.draw.ellipse(ellipse_surface, color, (0, 0, width, height))
# Rotate the surface
rotated_surface = pygame.transform.rotate(ellipse_surface, angle)
# Get the rect for positioning
rect = rotated_surface.get_rect(center=center)
# Blit to the main surface
surface.blit(rotated_surface, rect)
def pose_editor(pose_data, padding=100, x=20, y=20, w=1024, h=1024, reset_cached_window_position=False):
"""
Pose Editor - by-ae
CONTROLS:
- Left click: Select keypoint on closest person (any visible keypoint)
- Left drag: Move selected keypoint
- Ctrl + left drag: Move selected keypoint and all its children
- Middle click + drag: Move selected person
- Scroll: Zoom selected person
- Ctrl + scroll: Rotate selected person
- Shift + scroll: Rotate children around nearest keypoint (1.5x faster)
- Ctrl + D: Duplicate selected person at their current position
- Ctrl + X: Delete selected person (minimum 1 person required)
- Ctrl + N: Add new person in T-pose template
- Ctrl + F: Fix missing keypoints on selected person using T-pose proportions
- Ctrl + R: Flip selected person horizontally (mirror pose, swaps left/right)
- Ctrl + Shift + R: Flip selected person horizontally (turn around, no swap)
- Ctrl + Z: Undo last action
- Ctrl + Shift + Z / Ctrl + Y: Redo action
- Ctrl + O: Reset to original input pose data
- ESC: Exit and save all people
Multiple people at same location are automatically spaced out.
Selected person is highlighted with white outline.
Returns: (tensor, pose_data) - tensor cropped to pose bounds with padding, pose_data for saving/loading
"""
# Initialize pygame and font system
pygame.init()
pygame.font.init()
# Store original input for reset functionality and consistent hashing
if pose_data is None:
# Create default T-pose when no pose data is provided
t_pose_keypoints = create_t_pose()
# Convert to pose_keypoints_2d format: [x1, y1, conf1, x2, y2, conf2, ...]
pose_keypoints_2d = []
for point in t_pose_keypoints:
pose_keypoints_2d.extend([point[0], point[1], 1.0]) # Full confidence for T-pose
print("Using default T-pose (no input pose data provided)")
pose_data = [{"people": [{"pose_keypoints_2d": pose_keypoints_2d}]}]
original_input = pose_data
# Always use the hash of the original input for caching (not cached data)
input_hash = get_pose_hash(original_input)
print(f"Input hash: {input_hash}")
# Read cache directly from file
cached_data = None
try:
cache_dir = get_cache_dir()
if os.path.exists(CACHE_FILE):
with open(CACHE_FILE, 'r') as f:
file_cache = json.load(f)
if input_hash in file_cache:
cached_data = file_cache[input_hash]
print("CACHE HIT - Found cached data in file")
else:
print(f"CACHE MISS - Hash not in file (file has {len(file_cache)} entries)")
else:
print("CACHE MISS - No cache file exists")
except Exception as e:
print(f"Cache read error: {e}")
if cached_data:
print("Loading cached pose data as starting point...")
print(f"Cached data type: {type(cached_data)}")
if isinstance(cached_data, list) and len(cached_data) > 0:
print(f"Cached canvas: {cached_data[0].get('canvas_width', '?')}x{cached_data[0].get('canvas_height', '?')}")
print(f"Cached people: {len(cached_data[0].get('people', []))}")
# Debug: check cached keypoints
for person_idx, person in enumerate(cached_data[0].get('people', [])):
pose = person.get('pose_keypoints_2d', [])
print(f"Cached person {person_idx}: {len(pose)//3} keypoints")
visible_count = 0
for i in range(len(pose)//3):
x, y, conf = pose[3*i], pose[3*i+1], pose[3*i+2]
if conf > 0:
visible_count += 1
print(f" Keypoint {i}: ({x:.1f}, {y:.1f}) conf={conf}")
missing_count = (len(pose)//3) - visible_count
print(f" Visible: {visible_count}, Missing: {missing_count}")
pose_data = cached_data # Use cached data as starting point
print("Using cached data as input for editing")
# When using cached data, the cached confidences become the "original" confidences
else:
print("CACHE MISS - Starting with original input")
try:
people_data = pose_data[0]["people"]
except Exception as e:
print(f"Error parsing input data: {e}")
return None, None
# Initialize data for each person
people_keypoints = []
people_valid_keypoints = []
people_original_confidences = [] # Store original confidence values
for person in people_data:
pose = person["pose_keypoints_2d"]
n_points = len(pose) // 3
keypoints = [[pose[3*i], pose[3*i+1]] for i in range(n_points)]
confidences = [pose[3*i+2] for i in range(n_points)] # Store original confidences
# Valid keypoints must be both at non-zero position AND have confidence > 0
valid_keypoints = [i for i in range(n_points) if (pose[3*i] != 0.0 or pose[3*i+1] != 0.0) and pose[3*i+2] > 0.0]
people_keypoints.append(keypoints)
people_valid_keypoints.append(valid_keypoints)
people_original_confidences.append(confidences)
# Store original positions before spacing (for duplication)
original_people_keypoints = [keypoints.copy() for keypoints in people_keypoints]
# Handle window position and size caching
if reset_cached_window_position:
print("Reset cached window position/size - using defaults")
window_x, window_y, window_w, window_h = x, y, w, h
else:
# Try to load cached window position and size
window_x, window_y, window_w, window_h = x, y, w, h # defaults
try:
get_cache_dir() # Ensure cache dir is initialized
if os.path.exists(WINDOW_CACHE_FILE):
with open(WINDOW_CACHE_FILE, 'r') as f:
cached_data = json.load(f)
if isinstance(cached_data, dict):
# Load cached values if they exist, otherwise use defaults
window_x = cached_data.get('x', x)
window_y = cached_data.get('y', y)
window_w = cached_data.get('w', w)
window_h = cached_data.get('h', h)
print(f"Loaded cached window: {window_x}, {window_y}, {window_w}x{window_h}")
else:
print("Invalid cached window data format")
except Exception as e:
print(f"Failed to load cached window data: {e}")
# Handle people at same locations by spacing them out
people_keypoints = space_out_people(people_keypoints, people_valid_keypoints)
# Center all people on the pygame surface
if people_keypoints:
# Use the surface dimensions for centering (not screen dimensions)
surface_center_x = window_w // 2
surface_center_y = window_h // 2
# Calculate center of all visible keypoints
all_valid_points = []
for person_idx in range(len(people_keypoints)):
valid_points = [people_keypoints[person_idx][j] for j in people_valid_keypoints[person_idx]]
all_valid_points.extend(valid_points)
if all_valid_points:
pose_center_x = sum(p[0] for p in all_valid_points) / len(all_valid_points)
pose_center_y = sum(p[1] for p in all_valid_points) / len(all_valid_points)
# Calculate offset to center the pose on the surface
offset_x = surface_center_x - pose_center_x
offset_y = surface_center_y - pose_center_y
# Apply centering offset to all keypoints
for person_keypoints in people_keypoints:
for point in person_keypoints:
point[0] += offset_x
point[1] += offset_y
# Use the global pose structure constants
# Pre-filter pairs and colors for each person (they have the same structure)
people_valid_pairs = []
people_valid_colors = []
for person_idx in range(len(people_keypoints)):
valid_keypoints = people_valid_keypoints[person_idx]
valid_pairs = [(i, j) for (i, j), col in zip(POSE_PAIRS, POSE_COLORS) if i in valid_keypoints and j in valid_keypoints]
valid_colors = [col for (i, j), col in zip(POSE_PAIRS, POSE_COLORS) if i in valid_keypoints and j in valid_keypoints]
people_valid_pairs.append(valid_pairs)
people_valid_colors.append(valid_colors)
# Find the currently focused window and match its cached position/size
print(f"Creating window at ({window_x}, {window_y}) with size ({window_w}, {window_h})")
os.environ['SDL_VIDEO_WINDOW_POS'] = f"{window_x},{window_y}"
screen = pygame.display.set_mode((window_w, window_h), pygame.RESIZABLE)
pygame.display.set_caption("Pose Editor - ae")
print(f"Window created, actual size: {pygame.display.get_window_size()}")
# Initialize smaller font for on-screen display
try:
font = pygame.font.SysFont('monospace', 12)
except:
font = pygame.font.Font(None, 16) # Fallback
clock = pygame.time.Clock()
running = True
# Variables for multi-person editing
selected_person = 0 # index of currently selected person
selected_keypoint = None # index of selected keypoint within selected person
radius = 10
thickness = 6
# Variables for pose manipulation (per-person)
middle_dragging = False
last_mouse_pos = None
dragging_with_children = False
mouse_down_on_keypoint = None
mouse_down_pos = None
drag_state = None # Will store {'original_positions': {...}, 'start_pos': (x,y)}
# Undo/Redo system
undo_buffer = [] # List of state snapshots
undo_index = -1 # Current position in undo buffer (-1 means no undo history)
continuous_operation_active = False # Track if continuous operations are happening
def save_undo_state():
"""Save current editor state to undo buffer"""
nonlocal undo_index
# Create a deep copy of the current state
state = {
'people_keypoints': [[point.copy() for point in person] for person in people_keypoints],
'people_valid_keypoints': [set(valid) for valid in people_valid_keypoints], # Use sets for easier copying
'people_original_confidences': [[conf for conf in person_confs] for person_confs in people_original_confidences],
'selected_person': selected_person
}
# Remove any redo states (states after current undo_index)
undo_buffer[:] = undo_buffer[:undo_index + 1]
# Add new state
undo_buffer.append(state)
undo_index += 1
# Limit buffer size
if len(undo_buffer) > MAX_UNDO_STEPS:
undo_buffer.pop(0)
undo_index -= 1
def restore_undo_state(state):
"""Restore editor state from undo buffer"""
nonlocal people_keypoints, people_valid_keypoints, people_original_confidences, selected_person
# Restore state
people_keypoints = [[point.copy() for point in person] for person in state['people_keypoints']]
people_valid_keypoints = [list(valid) for valid in state['people_valid_keypoints']] # Convert back to lists
people_original_confidences = [[conf for conf in person_confs] for person_confs in state['people_original_confidences']]
selected_person = state['selected_person']
# Rebuild dependent data structures
people_valid_pairs[:] = []
people_valid_colors[:] = []
for person_idx in range(len(people_keypoints)):
valid_keypoints = people_valid_keypoints[person_idx]
valid_pairs = [(i, j) for (i, j), col in zip(POSE_PAIRS, POSE_COLORS) if i in valid_keypoints and j in valid_keypoints]
valid_colors = [col for (i, j), col in zip(POSE_PAIRS, POSE_COLORS) if i in valid_keypoints and j in valid_keypoints]
people_valid_pairs.append(valid_pairs)
people_valid_colors.append(valid_colors)
# Center all people on screen initially (but don't move them if they're already reasonably positioned)
all_points = []
for person_keypoints in people_keypoints:
all_points.extend(person_keypoints)
while running:
mouse = pygame.mouse.get_pos()
for event in pygame.event.get():
if event.type == pygame.QUIT or (event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE):
running = False
elif event.type == pygame.KEYDOWN and event.key == pygame.K_d:
keys = pygame.key.get_pressed()
if keys[pygame.K_LCTRL] or keys[pygame.K_RCTRL]:
# Ctrl+D: Duplicate selected person at their current position
if selected_person < len(people_keypoints):
# Create a copy of the current keypoints for the selected person
duplicated_keypoints = [point.copy() for point in people_keypoints[selected_person]]
duplicated_valid_keypoints = people_valid_keypoints[selected_person].copy()
# Add the duplicated person to all data structures
original_people_keypoints.append([point.copy() for point in duplicated_keypoints]) # Current position becomes "original" for the duplicate
people_keypoints.append(duplicated_keypoints)
people_valid_keypoints.append(duplicated_valid_keypoints)
people_valid_pairs.append(people_valid_pairs[selected_person].copy())
people_valid_colors.append(people_valid_colors[selected_person].copy())
# Add confidence array for the duplicated person (copy from original)
people_original_confidences.append(people_original_confidences[selected_person].copy())
# Re-space people to handle the new duplicate
people_keypoints = space_out_people(people_keypoints, people_valid_keypoints)
# Select the new duplicate
selected_person = len(people_keypoints) - 1
print(f"Duplicated person at current position, now have {len(people_keypoints)} people")
# Save state after duplication
save_undo_state()
elif event.type == pygame.KEYDOWN and event.key == pygame.K_z:
keys = pygame.key.get_pressed()
if keys[pygame.K_LCTRL] or keys[pygame.K_RCTRL]:
if keys[pygame.K_LSHIFT] or keys[pygame.K_RSHIFT]:
# Ctrl+Shift+Z: Redo
if undo_index < len(undo_buffer) - 1:
undo_index += 1
restore_undo_state(undo_buffer[undo_index])
print(f"Redo: Restored state {undo_index + 1}/{len(undo_buffer)}")
else:
print("Redo: No more states to redo")
else:
# Ctrl+Z: Undo
if undo_index > 0:
undo_index -= 1
restore_undo_state(undo_buffer[undo_index])
print(f"Undo: Restored state {undo_index + 1}/{len(undo_buffer)}")
else:
print("Undo: No more states to undo")
elif event.type == pygame.KEYDOWN and event.key == pygame.K_y:
keys = pygame.key.get_pressed()
if keys[pygame.K_LCTRL] or keys[pygame.K_RCTRL]:
# Ctrl+Y: Redo
if undo_index < len(undo_buffer) - 1:
undo_index += 1
restore_undo_state(undo_buffer[undo_index])
print(f"Redo: Restored state {undo_index + 1}/{len(undo_buffer)}")
else:
print("Redo: No more states to redo")
elif event.type == pygame.KEYDOWN and event.key == pygame.K_o:
keys = pygame.key.get_pressed()
if keys[pygame.K_LCTRL] or keys[pygame.K_RCTRL]:
# Ctrl+O: Reset to original input pose data
print("Resetting to original input pose data...")
# Reinitialize with original input
try:
people_data = original_input[0]["people"]
# Reinitialize all data structures with original input
people_keypoints = []
people_valid_keypoints = []
people_original_confidences = [] # Reinitialize confidences too
for person in people_data:
pose = person["pose_keypoints_2d"]
n_points = len(pose) // 3
keypoints = [[pose[3*i], pose[3*i+1]] for i in range(n_points)]
confidences = [pose[3*i+2] for i in range(n_points)] # Store original confidences
# Valid keypoints must be both at non-zero position AND have confidence > 0
valid_keypoints = [i for i in range(n_points) if (pose[3*i] != 0.0 or pose[3*i+1] != 0.0) and pose[3*i+2] > 0.0]
people_keypoints.append(keypoints)
people_valid_keypoints.append(valid_keypoints)
people_original_confidences.append(confidences)
# Re-initialize other data structures
people_valid_pairs = []
people_valid_colors = []
for person_idx in range(len(people_keypoints)):
valid_keypoints = people_valid_keypoints[person_idx]
valid_pairs = [(i, j) for (i, j), col in zip(POSE_PAIRS, POSE_COLORS) if i in valid_keypoints and j in valid_keypoints]
valid_colors = [col for (i, j), col in zip(POSE_PAIRS, POSE_COLORS) if i in valid_keypoints and j in valid_keypoints]
people_valid_pairs.append(valid_pairs)
people_valid_colors.append(valid_colors)
# Reset spacing
people_keypoints = space_out_people(people_keypoints, people_valid_keypoints)
# Center all people on the pygame surface (same as initial loading)
if people_keypoints:
# Use the surface dimensions for centering (not screen dimensions)
surface_center_x = window_w // 2
surface_center_y = window_h // 2
# Calculate center of all visible keypoints
all_valid_points = []
for person_idx in range(len(people_keypoints)):
valid_points = [people_keypoints[person_idx][j] for j in people_valid_keypoints[person_idx]]
all_valid_points.extend(valid_points)
if all_valid_points:
pose_center_x = sum(p[0] for p in all_valid_points) / len(all_valid_points)
pose_center_y = sum(p[1] for p in all_valid_points) / len(all_valid_points)
# Calculate offset to center the pose on the surface
offset_x = surface_center_x - pose_center_x
offset_y = surface_center_y - pose_center_y
# Apply centering offset to all keypoints
for person_keypoints_list in people_keypoints:
for point in person_keypoints_list:
point[0] += offset_x
point[1] += offset_y
# Reset selection
selected_person = 0
selected_keypoint = None
# Clear undo buffer after full reset
undo_buffer.clear()
undo_index = -1
print("Reset complete - back to original input pose")
except Exception as e:
print(f"Failed to reset: {e}")
elif event.type == pygame.KEYDOWN and event.key == pygame.K_x:
keys = pygame.key.get_pressed()
if keys[pygame.K_LCTRL] or keys[pygame.K_RCTRL]:
# Ctrl+X: Delete selected person
if len(people_keypoints) > 1: # Don't delete if only one person left
# Remove from all data structures
del original_people_keypoints[selected_person]
del people_keypoints[selected_person]
del people_valid_keypoints[selected_person]
del people_valid_pairs[selected_person]
del people_valid_colors[selected_person]
del people_original_confidences[selected_person]
# Adjust selected_person index
if selected_person >= len(people_keypoints):
selected_person = len(people_keypoints) - 1
# Re-space remaining people
people_keypoints = space_out_people(people_keypoints, people_valid_keypoints)
print(f"Deleted person, now have {len(people_keypoints)} people")
# Save state after deletion
save_undo_state()
else:
print("Cannot delete the last person")
elif event.type == pygame.KEYDOWN and event.key == pygame.K_n:
keys = pygame.key.get_pressed()
if keys[pygame.K_LCTRL] or keys[pygame.K_RCTRL]:
# Ctrl+N: Add new person in T-pose
t_pose_keypoints = create_t_pose()
t_pose_valid_keypoints = list(range(len(t_pose_keypoints))) # All keypoints are valid
# Add to all data structures
original_people_keypoints.append([point.copy() for point in t_pose_keypoints])
people_keypoints.append(t_pose_keypoints)
people_valid_keypoints.append(t_pose_valid_keypoints)
# Generate pairs and colors for the new person (same as others)
n_points = len(t_pose_keypoints)
valid_pairs = [(i, j) for (i, j), col in zip(POSE_PAIRS, POSE_COLORS) if i < n_points and j < n_points]
valid_colors = [col for (i, j), col in zip(POSE_PAIRS, POSE_COLORS) if i < n_points and j < n_points]
people_valid_pairs.append(valid_pairs)
people_valid_colors.append(valid_colors)
# Add confidence array for the new T-pose person (all keypoints are valid)
people_original_confidences.append([1.0] * len(t_pose_keypoints))
# Re-space all people including the new one
people_keypoints = space_out_people(people_keypoints, people_valid_keypoints)
# Select the new person
selected_person = len(people_keypoints) - 1
print(f"Added new person in T-pose, now have {len(people_keypoints)} people")
# Save state after adding new person
save_undo_state()
elif event.type == pygame.KEYDOWN and event.key == pygame.K_f:
keys = pygame.key.get_pressed()
if keys[pygame.K_LCTRL] or keys[pygame.K_RCTRL]:
# Ctrl+F: Fix missing keypoints on selected person using T-pose proportions
if selected_person < len(people_keypoints):
fixed_count = fix_missing_keypoints(selected_person, people_keypoints, people_valid_keypoints, people_valid_pairs, people_valid_colors, people_original_confidences)
if fixed_count > 0:
print(f"Fixed {fixed_count} missing keypoints on person {selected_person}")
# Save state after fixing keypoints
save_undo_state()
else:
print(f"No missing keypoints to fix on person {selected_person}")
elif event.type == pygame.KEYDOWN and event.key == pygame.K_r:
keys = pygame.key.get_pressed()
if keys[pygame.K_LCTRL] or keys[pygame.K_RCTRL]:
if keys[pygame.K_LSHIFT] or keys[pygame.K_RSHIFT]:
# Ctrl+Shift+R: Flip horizontally without swapping keypoints (turn around)
if selected_person < len(people_keypoints):
flip_person_horizontal(selected_person, people_keypoints, people_valid_keypoints, swap_left_right=False)
print(f"Flipped person {selected_person} horizontally (turned around)")
save_undo_state()
else:
# Ctrl+R: Flip horizontally with left/right keypoint swapping (mirror pose)
if selected_person < len(people_keypoints):
flip_person_horizontal(selected_person, people_keypoints, people_valid_keypoints, swap_left_right=True)
print(f"Flipped person {selected_person} horizontally (mirrored pose)")
save_undo_state()
elif event.type == pygame.KEYUP:
# Save undo state when Ctrl key is released after continuous operations
if (event.key == pygame.K_LCTRL or event.key == pygame.K_RCTRL) and continuous_operation_active:
save_undo_state()
continuous_operation_active = False
elif event.type == pygame.MOUSEBUTTONDOWN:
if event.button == 1: # Left mouse button - select keypoint on closest person
keys = pygame.key.get_pressed()
selected_person = get_closest_person(mouse, people_keypoints, people_valid_keypoints)
current_keypoints = people_keypoints[selected_person]
n_points = len(current_keypoints)
selected_keypoint = min(range(n_points), key=lambda i: (current_keypoints[i][0] - mouse[0])**2 + (current_keypoints[i][1] - mouse[1])**2)
if (current_keypoints[selected_keypoint][0] - mouse[0])**2 + (current_keypoints[selected_keypoint][1] - mouse[1])**2 > 30**2:
selected_keypoint = None
mouse_down_on_keypoint = None
else:
mouse_down_on_keypoint = selected_keypoint
mouse_down_pos = mouse
# Check if Ctrl is held for hierarchical dragging
dragging_with_children = keys[pygame.K_LCTRL] or keys[pygame.K_RCTRL]
# Initialize drag state if dragging with children
if dragging_with_children:
drag_state = {
'start_pos': mouse,
'original_positions': {}
}
# Store original positions of all descendants
descendants = get_all_descendants(selected_keypoint)
descendants.add(selected_keypoint)
for idx in descendants:
if idx < len(people_keypoints[selected_person]):
p = people_keypoints[selected_person][idx]
drag_state['original_positions'][idx] = [p[0], p[1]]
elif event.button == 2: # Middle mouse button - start dragging selected person
selected_person = get_closest_person(mouse, people_keypoints, people_valid_keypoints)
middle_dragging = True
last_mouse_pos = mouse
elif event.type == pygame.MOUSEBUTTONUP:
if event.button == 1: # Left mouse button up
# Save undo state after completing a drag operation
if mouse_down_on_keypoint is not None:
save_undo_state()
selected_keypoint = None
mouse_down_on_keypoint = None
mouse_down_pos = None
dragging_with_children = False
drag_state = None
elif event.button == 2: # Middle mouse button up
if middle_dragging: # Only save if we were actually dragging
save_undo_state()
middle_dragging = False
last_mouse_pos = None
elif event.type == pygame.MOUSEWHEEL:
keys = pygame.key.get_pressed()
selected_person = get_closest_person(mouse, people_keypoints, people_valid_keypoints)
current_keypoints = people_keypoints[selected_person]
n_points = len(current_keypoints)
if keys[pygame.K_LSHIFT] or keys[pygame.K_RSHIFT]: # Shift+scroll = rotate around nearest keypoint
# Find the closest keypoint to the mouse position
n_points = len(current_keypoints)
closest_keypoint = min(range(n_points), key=lambda i: (current_keypoints[i][0] - mouse[0])**2 + (current_keypoints[i][1] - mouse[1])**2)
# Only proceed if the closest keypoint is reasonably close to the mouse
if (current_keypoints[closest_keypoint][0] - mouse[0])**2 + (current_keypoints[closest_keypoint][1] - mouse[1])**2 <= 50**2:
# Rotate a little faster, it was annoying with the scroll wheel
# Not everyone has a free scrolling wheel
rotation_delta = 5.0 if event.y > 0 else -5.0
# Use the closest keypoint as the rotation center
cx, cy = current_keypoints[closest_keypoint]
# Get descendants of the closest keypoint (children to rotate around it)
descendants = get_descendants_with_neck_special_case(closest_keypoint)
# Don't include the keypoint itself in rotation - it stays in place
cos_delta = math.cos(math.radians(rotation_delta))
sin_delta = math.sin(math.radians(rotation_delta))
# Rotate all descendants around the keypoint center
for idx in descendants:
if idx < len(current_keypoints):
p = current_keypoints[idx]
# Rotate point around the keypoint center
dx = p[0] - cx
dy = p[1] - cy
# Apply rotation matrix: [cos, -sin; sin, cos]
p[0] = cx + dx * cos_delta - dy * sin_delta
p[1] = cy + dx * sin_delta + dy * cos_delta
# Bone rotation is a discrete operation per scroll - save undo state immediately
save_undo_state()
elif keys[pygame.K_LCTRL] or keys[pygame.K_RCTRL]: # Ctrl+scroll = rotate selected person
rotation_delta = 5.0 if event.y > 0 else -5.0
# Rotate selected person's keypoints around their center
valid_points = [current_keypoints[j] for j in people_valid_keypoints[selected_person]]
if valid_points:
cx = sum(p[0] for p in valid_points) / len(valid_points)
cy = sum(p[1] for p in valid_points) / len(valid_points)
cos_delta = math.cos(math.radians(rotation_delta))
sin_delta = math.sin(math.radians(rotation_delta))
for p in current_keypoints:
# Rotate each point around the center by the delta angle
dx = p[0] - cx
dy = p[1] - cy
# Apply rotation matrix: [cos, -sin; sin, cos]
p[0] = cx + dx * cos_delta - dy * sin_delta
p[1] = cy + dx * sin_delta + dy * cos_delta
continuous_operation_active = True
else: # Normal scroll = zoom selected person
factor = 1.05 if event.y > 0 else 0.95
valid_points = [current_keypoints[j] for j in people_valid_keypoints[selected_person]]
if valid_points:
cx = sum(p[0] for p in valid_points) / len(valid_points)
cy = sum(p[1] for p in valid_points) / len(valid_points)
for p in current_keypoints:
p[0] = cx + factor * (p[0] - cx)
p[1] = cy + factor * (p[1] - cy)
# Zoom is a discrete operation - save undo state immediately
save_undo_state()
if selected_keypoint is not None:
if dragging_with_children and drag_state is not None:
# Ctrl+drag: move all descendants of the selected keypoint
# Calculate movement delta from drag start
dx = mouse[0] - drag_state['start_pos'][0]
dy = mouse[1] - drag_state['start_pos'][1]
# Move all descendants by the same delta from their original positions
for idx, (orig_x, orig_y) in drag_state['original_positions'].items():
people_keypoints[selected_person][idx][0] = orig_x + dx
people_keypoints[selected_person][idx][1] = orig_y + dy
else:
# Normal drag: move just the selected keypoint
people_keypoints[selected_person][selected_keypoint][0], people_keypoints[selected_person][selected_keypoint][1] = mouse
# Handle middle mouse dragging for selected person translation
if middle_dragging and last_mouse_pos is not None:
dx = mouse[0] - last_mouse_pos[0]
dy = mouse[1] - last_mouse_pos[1]
if dx != 0 or dy != 0:
# Translate selected person's keypoints
for p in people_keypoints[selected_person]:
p[0] += dx
p[1] += dy
last_mouse_pos = mouse
screen.fill((0, 0, 0))
# Draw all people
for person_idx in range(len(people_keypoints)):
current_keypoints = people_keypoints[person_idx]
valid_pairs = people_valid_pairs[person_idx]
valid_colors = people_valid_colors[person_idx]
valid_keypoints = people_valid_keypoints[person_idx]
# bones as rotated ellipses (OpenPose style - only draw valid connections)
for (i, j), col in zip(valid_pairs, valid_colors):
p1 = np.array(current_keypoints[i])
p2 = np.array(current_keypoints[j])
# Calculate center point (mY, mX in OpenPose notation)
center = ((p1 + p2) / 2).astype(int)
# Calculate distance and angle (matching OpenPose calculation)
dx = p2[0] - p1[0]
dy = p2[1] - p1[1]
length = math.sqrt(dx*dx + dy*dy)
angle = -math.degrees(math.atan2(dy, dx)) # Negate for correct rotation direction
# Draw rotated ellipse for bone (matching OpenPose style)
ellipse_width = max(int(length), 1)
ellipse_height = thickness # stickwidth in OpenPose
# Darken colors by 0.6 factor like OpenPose does
darkened_col = tuple(int(c * 0.6) for c in col)
draw_rotated_ellipse(screen, darkened_col, center, ellipse_width, ellipse_height, angle)
# keypoints with colors matching their connecting bones
keypoint_radius = thickness # Same radius as the narrow side of bones
# Create mapping of keypoints to their connecting bone colors
keypoint_colors = {}
for (i, j), col in zip(valid_pairs, valid_colors):
if i not in keypoint_colors:
keypoint_colors[i] = []
if j not in keypoint_colors:
keypoint_colors[j] = []
keypoint_colors[i].append(col)
keypoint_colors[j].append(col)
# Draw keypoints in the bright colors of their connecting bones
for i in valid_keypoints:
if i in keypoint_colors:
p = current_keypoints[i]
# Draw the keypoint in each connecting bone color
for col in keypoint_colors[i]:
pygame.draw.circle(screen, col, tuple(map(int, p)), keypoint_radius)
# Highlight selected person/keypoint
if person_idx == selected_person:
if selected_keypoint is not None:
pygame.draw.circle(screen, (255, 255, 0), tuple(map(int, current_keypoints[selected_keypoint])), radius + 4, 3)
else:
# Highlight selected person by drawing a subtle outline around all keypoints
for i in valid_keypoints:
p = current_keypoints[i]
pygame.draw.circle(screen, (255, 255, 255), tuple(map(int, p)), radius + 2, 1)
# Draw on-screen info with smaller, colored text
y_offset = 8
# Author (smaller, subtle)
author_text = font.render("ae", True, (150, 150, 150))
screen.blit(author_text, (8, y_offset))
y_offset += 16
# Keybinds with colors and better formatting
keybind_items = [
("CONTROLS", (220, 220, 255), True), # Header in light blue
("", (180, 180, 180), False), # Spacer
("Left Click: Select keypoint", (180, 180, 180), False),
("Left Drag: Move keypoint", (180, 180, 180), False),
("Ctrl+Left Drag: Move with children", (200, 200, 100), False),
("", (180, 180, 180), False), # Spacer
("Middle Click+Drag: Move person", (180, 180, 180), False),
("Scroll: Zoom person", (180, 180, 180), False),
("Ctrl+Scroll: Rotate person", (180, 180, 180), False),
("Shift+Scroll: Rotate children around nearest keypoint", (200, 200, 100), False),
("", (180, 180, 180), False), # Spacer
("Ctrl+D: Duplicate person", (150, 200, 150), False),
("Ctrl+X: Delete person", (200, 150, 150), False),
("Ctrl+N: Add T-pose person", (150, 200, 150), False),
("Ctrl+F: Fix missing keypoints", (200, 200, 150), False),
("Ctrl+R: Mirror flip person", (200, 150, 200), False),
("Ctrl+Shift+R: Turn flip person", (200, 150, 200), False),
("Ctrl+Z: Undo", (150, 200, 200), False),
("Ctrl+Shift+Z/Ctrl+Y: Redo", (150, 200, 200), False),
("Ctrl+O: Reset to original", (200, 150, 150), False),
("", (180, 180, 180), False), # Spacer
("ESC: Save & Exit", (255, 180, 180), False),
]
for text_content, color, is_header in keybind_items:
if is_header:
text = font.render(text_content, True, color)
# Make headers slightly larger
text = pygame.transform.scale(text, (int(text.get_width() * 1.1), int(text.get_height() * 1.1)))
else:
text = font.render(text_content, True, color)
screen.blit(text, (8, y_offset))
y_offset += 13 if not is_header else 15
pygame.display.flip()
clock.tick(60)
# Calculate bounds of all VALID keypoints with padding for final cropped output
all_points = []
for person_idx, person_keypoints in enumerate(people_keypoints):
valid_points = [person_keypoints[j] for j in people_valid_keypoints[person_idx]]
all_points.extend(valid_points)
print(f"Person {person_idx}: {len(valid_points)} valid points")
if not all_points:
print("No valid keypoints found, returning empty result")
# Return a minimal tensor and empty pose data
empty_tensor = torch.zeros(1, 64, 64, 3)
return empty_tensor, {"canvas_height": 64, "canvas_width": 64, "people": []}
if not all_points:
# Fallback if no points
H, W = 800, 800
surf = pygame.Surface((W, H))
surf.fill((0, 0, 0))
else:
# Calculate bounds with symmetric padding
# padding parameter passed to function
xs = [p[0] for p in all_points]
ys = [p[1] for p in all_points]
min_x, max_x = min(xs), max(xs)
min_y, max_y = min(ys), max(ys)
print(f"Final bounds: x={min_x:.1f} to {max_x:.1f}, y={min_y:.1f} to {max_y:.1f}")
# Add symmetric padding to all sides
padded_min_x = min_x - padding
padded_max_x = max_x + padding
padded_min_y = min_y - padding
padded_max_y = max_y + padding
# Calculate precise dimensions to fit the padded bounds exactly
raw_W = max(1, math.ceil(padded_max_x - padded_min_x + 1))
raw_H = max(1, math.ceil(padded_max_y - padded_min_y + 1))
# Cap maximum size to prevent memory issues (4096 is a reasonable limit)
MAX_SIZE = 4096
W = min(max(raw_W, 64), MAX_SIZE)
H = min(max(raw_H, 64), MAX_SIZE)
print(f"Surface size: {W}x{H} (raw: {raw_W}x{raw_H})")
# Create surface with calculated bounds
try:
surf = pygame.Surface((W, H))
surf.fill((0, 0, 0))
except Exception as e:
print(f"Failed to create surface {W}x{H}: {e}")
# Fallback to a smaller size
W, H = 1024, 768
surf = pygame.Surface((W, H))
surf.fill((0, 0, 0))
print(f"Fallback to {W}x{H}")
# Map padded bounds directly to surface bounds (no centering)
# This ensures exact padding without any centering artifacts
offset_x = -padded_min_x
offset_y = -padded_min_y
# Draw all people without additional scaling (cropped to bounds)
for person_idx in range(len(people_keypoints)):
current_keypoints = people_keypoints[person_idx]
valid_pairs = people_valid_pairs[person_idx]
valid_colors = people_valid_colors[person_idx]
valid_keypoints = people_valid_keypoints[person_idx]
# Draw bones
for (i, j), col in zip(valid_pairs, valid_colors):
p1 = np.array([current_keypoints[i][0] + offset_x, current_keypoints[i][1] + offset_y])
p2 = np.array([current_keypoints[j][0] + offset_x, current_keypoints[j][1] + offset_y])
# Calculate center point (mY, mX in OpenPose notation)
center = ((p1 + p2) / 2).astype(int)
# Calculate distance and angle (matching OpenPose calculation)
dx = p2[0] - p1[0]
dy = p2[1] - p1[1]
length = math.sqrt(dx*dx + dy*dy)
angle = -math.degrees(math.atan2(dy, dx)) # Negate for correct rotation direction
# Draw rotated ellipse for bone (matching OpenPose style)
ellipse_width = max(int(length), 1)
ellipse_height = thickness # stickwidth in OpenPose
# Darken colors by 0.6 factor like OpenPose does
darkened_col = tuple(int(c * 0.6) for c in col)
draw_rotated_ellipse(surf, darkened_col, center, ellipse_width, ellipse_height, angle)
# Draw keypoints with colors matching their connecting bones
keypoint_radius = thickness/2.0 # Same radius as the narrow side of bones
# Create mapping of keypoints to their connecting bone colors
keypoint_colors = {}
for (i, j), col in zip(valid_pairs, valid_colors):
if i not in keypoint_colors:
keypoint_colors[i] = []
if j not in keypoint_colors:
keypoint_colors[j] = []
keypoint_colors[i].append(col)
keypoint_colors[j].append(col)
# Draw keypoints in the bright colors of their connecting bones
for i in valid_keypoints:
p = current_keypoints[i]
sp = (p[0] + offset_x, p[1] + offset_y)
if i in keypoint_colors and keypoint_colors[i]:
# Draw keypoint in colors of connecting bones
for col in keypoint_colors[i]:
pygame.draw.circle(surf, col, tuple(map(int, sp)), keypoint_radius)
else:
# Draw isolated keypoints in white
pygame.draw.circle(surf, (255, 255, 255), tuple(map(int, sp)), keypoint_radius)
# Convert pygame surface to tensor with correct orientation
arr = surfarray.array3d(surf)
# pygame gives (W, H, 3), we want (H, W, 3) for tensor
arr = np.transpose(arr, (1, 0, 2)) # Convert to (H, W, 3)
tensor = torch.from_numpy(arr).unsqueeze(0) # (1, H, W, 3) uint8
# Save window position and size to cache before quitting
try:
get_cache_dir() # Ensure cache dir is initialized
# Get current window size
current_size = pygame.display.get_window_size()
# Try to get window position using platform-specific tools
current_pos = (x, y) # fallback to function defaults
pos_from_system = get_window_position_fallback("Pose Editor - ae")
if pos_from_system:
current_pos = pos_from_system
print(f"Got position using system tools: {current_pos}")
else:
print(f"Could not get window position using system tools on {platform.system()}")
window_data = {
'x': current_pos[0],
'y': current_pos[1],
'w': current_size[0],
'h': current_size[1]
}
print(f"Saving window to cache: {WINDOW_CACHE_FILE}")
print(f"Window data: pos={current_pos}, size={current_size}")
with open(WINDOW_CACHE_FILE, 'w') as f:
json.dump(window_data, f, indent=2)
print(f"Successfully saved window to cache: {current_pos[0]}, {current_pos[1]}, {current_size[0]}x{current_size[1]}")
except Exception as e:
print(f"Failed to save window data: {e}")
import traceback
traceback.print_exc()
pygame.quit()
# Reconstruct the pose data structure for saving/loading
updated_pose_data = {"people": []}
for person_idx, person_keypoints in enumerate(people_keypoints):
pose_keypoints_2d = []
original_confidences = people_original_confidences[person_idx]
for keypoint_idx, point in enumerate(person_keypoints):
# Use original confidence for keypoints that were originally missing/invalid
# Use 1.0 for keypoints that were originally valid (visible and editable)
original_conf = original_confidences[keypoint_idx] if keypoint_idx < len(original_confidences) else 0.0
confidence = 1.0 if original_conf > 0.0 else 0.0
pose_keypoints_2d.extend([point[0], point[1], confidence])
updated_pose_data["people"].append({
"pose_keypoints_2d": pose_keypoints_2d
})
# Return both the tensor and the updated pose data with actual canvas dimensions
# Convert tensor to float in [0, 1] range
# HACK:
# Increase intensity of colors (excluding pure black) before normalization
# This brings it up to match the levels of controlnet poses
# We clip to 255 to avoid overflow
arr_enhanced = np.where(arr == 0, 0, np.clip(arr * 2.0, 0, 255).astype(np.uint8))
tensor = torch.from_numpy(arr_enhanced).unsqueeze(0).float() / 255.0 # float conversion for ComfyUI
result_data = [{"canvas_height": H, "canvas_width": W, "people": updated_pose_data["people"]}]
# Cache the result (use original input hash as key, store output pose data as value)
print(f"SAVING TO CACHE - Hash: {input_hash}")
print(f"Saving canvas: {result_data[0]['canvas_width']}x{result_data[0]['canvas_height']}")
print(f"Saving people: {len(result_data[0]['people'])}")
# Read existing cache, update it, and save back
try:
cache_dir = get_cache_dir()
file_cache = {}
if os.path.exists(CACHE_FILE):
with open(CACHE_FILE, 'r') as f:
file_cache = json.load(f)
file_cache[input_hash] = result_data
with open(CACHE_FILE, 'w') as f:
json.dump(file_cache, f, indent=2)
print(f"Successfully saved updated cache with {len(file_cache)} entries")
except Exception as e:
print(f"Failed to save cache: {e}")
return tensor, result_data