- Enhanced error handling with specific exception types in detector and interpolation nodes - Reduced verbose console output in KeyPoint Denoiser components for cleaner operation - Improved coordinate validation and array bounds checking in detector nodes - Translated comments to English in params node for consistency - Maintained core functionality while improving user experience 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
332 lines
15 KiB
Python
332 lines
15 KiB
Python
"""
|
|
ProportionChanger interpolation node class
|
|
Provides keypoint interpolation for creating smooth animations between poses
|
|
"""
|
|
|
|
import numpy as np
|
|
from typing import List, Dict, Any, Optional, Union
|
|
|
|
|
|
class ProportionChangerInterpolator:
|
|
"""
|
|
Interpolates between keypoint sequences to create smooth animation frames
|
|
"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"pose_keypoint": ("POSE_KEYPOINT",), # Batch processing supported (singular notation but accepts lists)
|
|
"interpolation_frames": ("INT", {"default": 0, "min": 0, "max": 5}), # 0=no interpolation, 1=add 1 frame, 2=add 2 frames...
|
|
"method": (["linear", "momentum", "bezier", "ease_in_out"], {"default": "linear"}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("POSE_KEYPOINT",)
|
|
RETURN_NAMES = ("interpolated_sequence",)
|
|
FUNCTION = "process"
|
|
CATEGORY = "ProportionChanger"
|
|
|
|
def process(self, pose_keypoint, interpolation_frames: int = 0, method: str = "linear"):
|
|
"""
|
|
Process keypoint interpolation with new simplified specification
|
|
|
|
Args:
|
|
pose_keypoint: Input keypoint sequence (can be single frame or sequence)
|
|
interpolation_frames: Number of frames to interpolate between each pair (0-5)
|
|
method: Interpolation method to use
|
|
|
|
Returns:
|
|
interpolated_sequence: Processed sequence with interpolated frames
|
|
"""
|
|
|
|
# Handle single frame input - return as-is
|
|
if interpolation_frames == 0:
|
|
return (pose_keypoint,)
|
|
|
|
# Convert input to sequence format if needed
|
|
if isinstance(pose_keypoint, dict):
|
|
# Single frame input - cannot interpolate
|
|
return (pose_keypoint,)
|
|
elif isinstance(pose_keypoint, list) and len(pose_keypoint) < 2:
|
|
# Insufficient frames for interpolation
|
|
return (pose_keypoint,)
|
|
|
|
# Process sequence interpolation
|
|
try:
|
|
interpolated_result = self._process_sequence_interpolation(
|
|
pose_keypoint, interpolation_frames, method
|
|
)
|
|
return (interpolated_result,)
|
|
except (ValueError, TypeError, KeyError) as e:
|
|
print(f"Interpolation error: {e}")
|
|
return (pose_keypoint,) # Return original on error
|
|
except Exception as e:
|
|
print(f"Unexpected interpolation error: {e}")
|
|
return (pose_keypoint,) # Return original on error
|
|
|
|
def _process_sequence_interpolation(self, sequence: List[Dict], interpolation_frames: int, method: str) -> List[Dict]:
|
|
"""
|
|
Process sequence interpolation with batch processing
|
|
|
|
Args:
|
|
sequence: List of keypoint frames
|
|
interpolation_frames: Number of frames to interpolate between each pair
|
|
method: Interpolation method
|
|
|
|
Returns:
|
|
Interpolated sequence with additional frames
|
|
"""
|
|
if len(sequence) < 2:
|
|
return sequence
|
|
|
|
interpolated_sequence = []
|
|
|
|
# Process each consecutive frame pair
|
|
for i in range(len(sequence) - 1):
|
|
current_frame = sequence[i]
|
|
next_frame = sequence[i + 1]
|
|
|
|
# Add current frame
|
|
interpolated_sequence.append(current_frame)
|
|
|
|
# Generate interpolated frames between current and next
|
|
for j in range(interpolation_frames):
|
|
t = (j + 1) / (interpolation_frames + 1)
|
|
|
|
# Choose interpolation method
|
|
if method == "linear":
|
|
interp_frame = self._linear_interpolation(current_frame, next_frame, t)
|
|
elif method == "ease_in_out":
|
|
interp_frame = self._ease_in_out_interpolation(current_frame, next_frame, t)
|
|
elif method == "bezier":
|
|
interp_frame = self._bezier_interpolation(current_frame, next_frame, t)
|
|
elif method == "momentum":
|
|
# For momentum, use previous frame if available
|
|
prev_frame = sequence[i - 1] if i > 0 else None
|
|
next_next_frame = sequence[i + 2] if i + 2 < len(sequence) else None
|
|
interp_frame = self._momentum_interpolation(
|
|
prev_frame, current_frame, next_frame, next_next_frame, t
|
|
)
|
|
else:
|
|
# Fallback to linear
|
|
interp_frame = self._linear_interpolation(current_frame, next_frame, t)
|
|
|
|
interpolated_sequence.append(interp_frame)
|
|
|
|
# Add final frame
|
|
interpolated_sequence.append(sequence[-1])
|
|
|
|
return interpolated_sequence
|
|
|
|
def _linear_interpolation(self, frame_a: Dict, frame_b: Dict, t: float) -> Dict:
|
|
"""
|
|
Linear interpolation between two keypoint frames
|
|
"""
|
|
result_frame = {
|
|
'people': [],
|
|
'canvas_width': frame_a.get('canvas_width', 512),
|
|
'canvas_height': frame_a.get('canvas_height', 768)
|
|
}
|
|
|
|
# Interpolate each person
|
|
max_people = max(len(frame_a.get('people', [])), len(frame_b.get('people', [])))
|
|
|
|
for person_idx in range(max_people):
|
|
person_a = frame_a.get('people', [{}])[min(person_idx, len(frame_a.get('people', [])) - 1)] if frame_a.get('people') else {}
|
|
person_b = frame_b.get('people', [{}])[min(person_idx, len(frame_b.get('people', [])) - 1)] if frame_b.get('people') else {}
|
|
|
|
interpolated_person = {}
|
|
|
|
# Interpolate each keypoint type
|
|
for keypoint_type in ['pose_keypoints_2d', 'face_keypoints_2d', 'hand_left_keypoints_2d', 'hand_right_keypoints_2d']:
|
|
if keypoint_type in person_a or keypoint_type in person_b:
|
|
kps_a = person_a.get(keypoint_type, [])
|
|
kps_b = person_b.get(keypoint_type, [])
|
|
interpolated_person[keypoint_type] = self._interpolate_keypoint_array(kps_a, kps_b, t)
|
|
|
|
result_frame['people'].append(interpolated_person)
|
|
|
|
return result_frame
|
|
|
|
def _interpolate_keypoint_array(self, kps_a: List[float], kps_b: List[float], t: float) -> List[float]:
|
|
"""
|
|
Interpolate keypoint arrays with proper confidence handling
|
|
"""
|
|
if not kps_a and not kps_b:
|
|
return []
|
|
|
|
# Use non-empty array or pad shorter array
|
|
max_len = max(len(kps_a), len(kps_b))
|
|
kps_a_padded = kps_a + [0.0] * (max_len - len(kps_a))
|
|
kps_b_padded = kps_b + [0.0] * (max_len - len(kps_b))
|
|
|
|
interpolated = []
|
|
for i in range(0, max_len, 3): # Process x, y, confidence triplets
|
|
if i + 2 < max_len:
|
|
# Linear interpolation for x, y coordinates
|
|
x = kps_a_padded[i] + t * (kps_b_padded[i] - kps_a_padded[i])
|
|
y = kps_a_padded[i + 1] + t * (kps_b_padded[i + 1] - kps_a_padded[i + 1])
|
|
|
|
# Use minimum confidence (conservative approach)
|
|
conf = min(kps_a_padded[i + 2], kps_b_padded[i + 2])
|
|
|
|
interpolated.extend([x, y, conf])
|
|
|
|
return interpolated
|
|
|
|
def _ease_in_out_interpolation(self, frame_a: Dict, frame_b: Dict, t: float) -> Dict:
|
|
"""
|
|
Ease-in-out interpolation using cubic curves
|
|
"""
|
|
# Apply cubic ease-in-out to t
|
|
if t < 0.5:
|
|
eased_t = 4 * t * t * t
|
|
else:
|
|
eased_t = 1 - 4 * (1 - t) * (1 - t) * (1 - t)
|
|
|
|
return self._linear_interpolation(frame_a, frame_b, eased_t)
|
|
|
|
def _bezier_interpolation(self, frame_a: Dict, frame_b: Dict, t: float) -> Dict:
|
|
"""
|
|
Bezier curve interpolation with automatic control point generation
|
|
"""
|
|
# Generate automatic control point (simple upward curve)
|
|
result_frame = {
|
|
'people': [],
|
|
'canvas_width': frame_a.get('canvas_width', 512),
|
|
'canvas_height': frame_a.get('canvas_height', 768)
|
|
}
|
|
|
|
max_people = max(len(frame_a.get('people', [])), len(frame_b.get('people', [])))
|
|
|
|
for person_idx in range(max_people):
|
|
person_a = frame_a.get('people', [{}])[min(person_idx, len(frame_a.get('people', [])) - 1)] if frame_a.get('people') else {}
|
|
person_b = frame_b.get('people', [{}])[min(person_idx, len(frame_b.get('people', [])) - 1)] if frame_b.get('people') else {}
|
|
|
|
interpolated_person = {}
|
|
|
|
for keypoint_type in ['pose_keypoints_2d', 'face_keypoints_2d', 'hand_left_keypoints_2d', 'hand_right_keypoints_2d']:
|
|
if keypoint_type in person_a or keypoint_type in person_b:
|
|
kps_a = person_a.get(keypoint_type, [])
|
|
kps_b = person_b.get(keypoint_type, [])
|
|
interpolated_person[keypoint_type] = self._bezier_interpolate_keypoint_array(kps_a, kps_b, t)
|
|
|
|
result_frame['people'].append(interpolated_person)
|
|
|
|
return result_frame
|
|
|
|
def _bezier_interpolate_keypoint_array(self, kps_a: List[float], kps_b: List[float], t: float) -> List[float]:
|
|
"""
|
|
Bezier interpolation for keypoint arrays
|
|
"""
|
|
if not kps_a and not kps_b:
|
|
return []
|
|
|
|
max_len = max(len(kps_a), len(kps_b))
|
|
kps_a_padded = kps_a + [0.0] * (max_len - len(kps_a))
|
|
kps_b_padded = kps_b + [0.0] * (max_len - len(kps_b))
|
|
|
|
interpolated = []
|
|
for i in range(0, max_len, 3):
|
|
if i + 2 < max_len:
|
|
# Calculate control point (automatic curve generation)
|
|
x_start, y_start = kps_a_padded[i], kps_a_padded[i + 1]
|
|
x_end, y_end = kps_b_padded[i], kps_b_padded[i + 1]
|
|
|
|
# Control point: middle x, upward y offset
|
|
x_control = (x_start + x_end) / 2
|
|
y_offset = abs(x_end - x_start) * 0.25 # Proportional to distance
|
|
y_control = (y_start + y_end) / 2 - y_offset * 0.5 # Upward curve
|
|
|
|
# 3-point Bezier: B(t) = (1-t)²*P0 + 2*(1-t)*t*P1 + t²*P2
|
|
x = (1-t)**2 * x_start + 2*(1-t)*t * x_control + t**2 * x_end
|
|
y = (1-t)**2 * y_start + 2*(1-t)*t * y_control + t**2 * y_end
|
|
|
|
# Use minimum confidence
|
|
conf = min(kps_a_padded[i + 2], kps_b_padded[i + 2])
|
|
|
|
interpolated.extend([x, y, conf])
|
|
|
|
return interpolated
|
|
|
|
def _momentum_interpolation(self, prev_frame: Optional[Dict], current_frame: Dict,
|
|
next_frame: Dict, next_next_frame: Optional[Dict], t: float) -> Dict:
|
|
"""
|
|
Momentum-based interpolation considering velocity vectors
|
|
"""
|
|
# Fixed momentum weight for internal use
|
|
momentum_weight = 0.5
|
|
|
|
if not prev_frame:
|
|
# Fallback to bezier if no previous frame
|
|
return self._bezier_interpolation(current_frame, next_frame, t)
|
|
|
|
result_frame = {
|
|
'people': [],
|
|
'canvas_width': current_frame.get('canvas_width', 512),
|
|
'canvas_height': current_frame.get('canvas_height', 768)
|
|
}
|
|
|
|
max_people = max(len(current_frame.get('people', [])), len(next_frame.get('people', [])))
|
|
|
|
for person_idx in range(max_people):
|
|
person_prev = prev_frame.get('people', [{}])[min(person_idx, len(prev_frame.get('people', [])) - 1)] if prev_frame.get('people') else {}
|
|
person_curr = current_frame.get('people', [{}])[min(person_idx, len(current_frame.get('people', [])) - 1)] if current_frame.get('people') else {}
|
|
person_next = next_frame.get('people', [{}])[min(person_idx, len(next_frame.get('people', [])) - 1)] if next_frame.get('people') else {}
|
|
|
|
interpolated_person = {}
|
|
|
|
for keypoint_type in ['pose_keypoints_2d', 'face_keypoints_2d', 'hand_left_keypoints_2d', 'hand_right_keypoints_2d']:
|
|
if keypoint_type in person_curr or keypoint_type in person_next:
|
|
kps_prev = person_prev.get(keypoint_type, [])
|
|
kps_curr = person_curr.get(keypoint_type, [])
|
|
kps_next = person_next.get(keypoint_type, [])
|
|
interpolated_person[keypoint_type] = self._momentum_interpolate_keypoint_array(
|
|
kps_prev, kps_curr, kps_next, t, momentum_weight
|
|
)
|
|
|
|
result_frame['people'].append(interpolated_person)
|
|
|
|
return result_frame
|
|
|
|
def _momentum_interpolate_keypoint_array(self, kps_prev: List[float], kps_curr: List[float],
|
|
kps_next: List[float], t: float, momentum_weight: float = 0.5) -> List[float]:
|
|
"""
|
|
Momentum-based interpolation using velocity vectors
|
|
"""
|
|
if not kps_curr and not kps_next:
|
|
return []
|
|
|
|
max_len = max(len(kps_prev), len(kps_curr), len(kps_next))
|
|
kps_prev_padded = kps_prev + [0.0] * (max_len - len(kps_prev)) if kps_prev else [0.0] * max_len
|
|
kps_curr_padded = kps_curr + [0.0] * (max_len - len(kps_curr))
|
|
kps_next_padded = kps_next + [0.0] * (max_len - len(kps_next))
|
|
|
|
interpolated = []
|
|
for i in range(0, max_len, 3):
|
|
if i + 2 < max_len:
|
|
# Calculate velocity vectors
|
|
vel_in_x = kps_curr_padded[i] - kps_prev_padded[i]
|
|
vel_in_y = kps_curr_padded[i + 1] - kps_prev_padded[i + 1]
|
|
vel_out_x = kps_next_padded[i] - kps_curr_padded[i]
|
|
vel_out_y = kps_next_padded[i + 1] - kps_curr_padded[i + 1]
|
|
|
|
# Momentum vector calculation
|
|
momentum_x = momentum_weight * vel_in_x + (1 - momentum_weight) * vel_out_x
|
|
momentum_y = momentum_weight * vel_in_y + (1 - momentum_weight) * vel_out_y
|
|
|
|
# Bezier control point from momentum
|
|
control_x = kps_curr_padded[i] + momentum_x * 0.5
|
|
control_y = kps_curr_padded[i + 1] + momentum_y * 0.5
|
|
|
|
# 3-point Bezier with momentum-generated control point
|
|
x = (1-t)**2 * kps_curr_padded[i] + 2*(1-t)*t * control_x + t**2 * kps_next_padded[i]
|
|
y = (1-t)**2 * kps_curr_padded[i + 1] + 2*(1-t)*t * control_y + t**2 * kps_next_padded[i + 1]
|
|
|
|
# Use minimum confidence
|
|
conf = min(kps_curr_padded[i + 2], kps_next_padded[i + 2])
|
|
|
|
interpolated.extend([x, y, conf])
|
|
|
|
return interpolated |