AlterProgramming's picture
add txt2img tab + infer_txt2img endpoint (SD 1.5)
7faaef2 verified
Raw
History Blame Contribute Delete
6.02 kB
"""studio.rigging.deform — forward kinematics + Linear Blend Skinning warp.
Pose representation: a dict mapping joint_name → local rotation (radians). Local
rotations propagate down the tree to produce a world rotation per joint; each
joint's deformed position is its parent's deformed position plus the rest offset
rotated by the parent's world rotation.
Pixel warping is LBS forward-splat: each foreground pixel computes its blended
output position as Σ_b weight_b · T_b(pixel), then bilinearly splats its color
into the output buffer. Subject preservation is structural: every output pixel
is a weighted combination of source pixels — no pixel is invented.
"""
from __future__ import annotations
from typing import Mapping, Tuple
import numpy as np
from pixel_cursor.rigging import BONES, JOINT_INDEX, NUM_JOINTS, PARENTS, Skeleton
def _eval_order() -> Tuple[int, ...]:
root_idx = PARENTS.index(-1)
order = [root_idx]
queue = [root_idx]
seen = {root_idx}
while queue:
parent = queue.pop(0)
for child, parent_idx in enumerate(PARENTS):
if parent_idx == parent and child not in seen:
order.append(child)
seen.add(child)
queue.append(child)
return tuple(order)
_EVAL_ORDER: Tuple[int, ...] = _eval_order()
def forward_kinematics(
rest: Skeleton,
local_rotations: Mapping[str, float] | None = None,
) -> Tuple[np.ndarray, np.ndarray]:
"""Run FK from a dict of local rotations.
Returns (deformed_positions shape (14, 2), world_rotations shape (14,)).
"""
local = np.zeros(NUM_JOINTS, dtype=np.float32)
if local_rotations:
for name, angle in local_rotations.items():
local[JOINT_INDEX[name]] = float(angle)
deformed = rest.positions.copy().astype(np.float32)
world_rot = np.zeros(NUM_JOINTS, dtype=np.float32)
root_idx = PARENTS.index(-1)
world_rot[root_idx] = local[root_idx]
for joint in _EVAL_ORDER:
if joint == root_idx:
continue
parent = PARENTS[joint]
world_rot[joint] = world_rot[parent] + local[joint]
rest_offset = rest.positions[joint] - rest.positions[parent]
c, s = np.cos(world_rot[parent]), np.sin(world_rot[parent])
rotated_y = c * rest_offset[0] - s * rest_offset[1]
rotated_x = s * rest_offset[0] + c * rest_offset[1]
deformed[joint, 0] = deformed[parent, 0] + rotated_y
deformed[joint, 1] = deformed[parent, 1] + rotated_x
return deformed.astype(np.float32), world_rot
def deform_sprite(
image: np.ndarray,
mask: np.ndarray,
rest: Skeleton,
weights: np.ndarray,
*,
local_rotations: Mapping[str, float] | None = None,
) -> Tuple[np.ndarray, np.ndarray]:
"""Apply LBS warp to a sprite. Returns (deformed_rgba (H, W, 4), deformed_mask (H, W) bool).
Algorithm:
1. FK produces deformed joint positions + per-joint world rotations.
2. For each foreground source pixel p:
T_b(p) = deformed[parent(b)] + R(world_rot[parent(b)]) · (p - rest[parent(b)])
out_pos = Σ_b weights[p, b] · T_b(p)
3. Bilinearly splat source RGBA into the output accumulator at out_pos.
4. Normalise accumulator by accumulated weight per output pixel.
"""
if mask.ndim != 2:
raise ValueError(f"mask must be 2D, got {mask.shape}")
H, W = mask.shape
deformed_pos, world_rot = forward_kinematics(rest, local_rotations)
if image.ndim == 2:
rgba_in = np.stack(
[image, image, image, np.full_like(image, 255)], axis=-1
)
elif image.shape[-1] == 3:
rgba_in = np.concatenate(
[image, np.full(image.shape[:2] + (1,), 255, dtype=np.uint8)], axis=-1
)
elif image.shape[-1] == 4:
rgba_in = image
else:
raise ValueError(f"unsupported image shape {image.shape}")
mask_bool = mask.astype(bool)
ys, xs = np.where(mask_bool)
if ys.size == 0:
return np.zeros((H, W, 4), dtype=np.uint8), np.zeros((H, W), dtype=bool)
src_coords = np.stack([ys, xs], axis=-1).astype(np.float32)
src_colors = rgba_in[ys, xs].astype(np.float32)
src_weights = weights[ys, xs] # (N_fg, N_BONES)
out_pos = np.zeros_like(src_coords)
for b, (parent_idx, _child_idx) in enumerate(BONES):
rot = float(world_rot[parent_idx])
c, s = np.cos(rot), np.sin(rot)
offset = src_coords - rest.positions[parent_idx]
ry = c * offset[:, 0] - s * offset[:, 1]
rx = s * offset[:, 0] + c * offset[:, 1]
rotated = np.stack([ry, rx], axis=-1)
per_bone_out = deformed_pos[parent_idx] + rotated
out_pos += src_weights[:, b : b + 1] * per_bone_out
accum = np.zeros((H, W, 4), dtype=np.float32)
wsum = np.zeros((H, W), dtype=np.float32)
oy = out_pos[:, 0]
ox = out_pos[:, 1]
oy_int = np.floor(oy).astype(np.int32)
ox_int = np.floor(ox).astype(np.int32)
fy = oy - oy_int
fx = ox - ox_int
for dy in (0, 1):
for dx in (0, 1):
wy = (1.0 - fy) if dy == 0 else fy
wx = (1.0 - fx) if dx == 0 else fx
wgt = wy * wx
yy = oy_int + dy
xx = ox_int + dx
valid = (yy >= 0) & (yy < H) & (xx >= 0) & (xx < W) & (wgt > 0)
if not valid.any():
continue
yy_v = yy[valid]
xx_v = xx[valid]
w_v = wgt[valid]
for ch in range(4):
np.add.at(accum[..., ch], (yy_v, xx_v), w_v * src_colors[valid, ch])
np.add.at(wsum, (yy_v, xx_v), w_v)
deformed_rgba = np.zeros((H, W, 4), dtype=np.uint8)
has_color = wsum > 0
np.divide(
accum,
np.maximum(wsum[..., None], 1e-12),
out=accum,
where=has_color[..., None],
)
deformed_rgba[has_color] = np.clip(accum[has_color], 0, 255).astype(np.uint8)
return deformed_rgba, has_color
__all__ = ["forward_kinematics", "deform_sprite"]