ColabWan / models /wan /scail /__init__.py
1ripon1's picture
Upload folder using huggingface_hub
7344bef verified
Raw
History Blame Contribute Delete
17.8 kB
from __future__ import annotations
__all__ = ["ScailPoseProcessor"]
import numpy as np
import torch
from PIL import Image, ImageOps
from shared.utils import files_locator as fl
from .nlf import load_multiperson_nlf_eager
from .scail_pose_align3d import solve_new_camera_params_central, solve_new_camera_params_down
from .scail_pose_dwpose import DWposeDetector
from .scail_pose_nlf import (
collect_smpl_poses,
intrinsic_matrix_from_field_of_view,
process_data_to_COCO_format,
process_video_multi_nlf,
process_video_nlf,
recollect_dwposes,
recollect_nlf,
render_multi_nlf_as_images,
render_nlf_as_images,
scale_faces,
shift_dwpose_according_to_nlf,
)
from .scail_pose_multi import change_poses_to_limit_num, get_largest_bbox_indices
def _pil_resample(name: str):
try:
return getattr(Image.Resampling, name)
except AttributeError: # Pillow<9
return getattr(Image, name)
_RESAMPLE_LANCZOS = _pil_resample("LANCZOS")
_RESAMPLE_NEAREST = _pil_resample("NEAREST")
def _tensor_frame_to_uint8_rgb(frame_chw: torch.Tensor) -> np.ndarray:
frame = frame_chw.detach()
if frame.dim() != 3:
raise ValueError(f"Expected CHW tensor, got shape {tuple(frame.shape)}")
if frame.shape[0] == 1:
frame = frame.expand(3, -1, -1)
if frame.shape[0] >= 4:
frame = frame[:3]
frame = frame.to(dtype=torch.float32)
if frame.numel() == 0:
return np.zeros((0, 0, 3), dtype=np.uint8)
if float(frame.min()) < 0.0:
frame = (frame + 1.0) * 127.5
elif float(frame.max()) <= 1.0:
frame = frame * 255.0
frame = frame.clamp(0, 255).to(torch.uint8)
return frame.permute(1, 2, 0).cpu().numpy()
def _tensor_mask_to_pil(mask: torch.Tensor) -> Image.Image:
m = mask.detach()
if m.dim() == 3:
m = m[0]
if m.dim() != 2:
raise ValueError(f"Expected HW (or 1HW) mask tensor, got shape {tuple(mask.shape)}")
m = m.to(dtype=torch.float32)
if m.numel() == 0:
return Image.fromarray(np.zeros((0, 0), dtype=np.uint8), mode="L")
if float(m.min()) < 0.0:
m = (m + 1.0) * 0.5
elif float(m.max()) > 1.0:
m = m / 255.0
m = m.clamp(0.0, 1.0)
m8 = (m * 255.0).to(torch.uint8).cpu().numpy()
return Image.fromarray(m8, mode="L")
class ScailPoseProcessor:
"""
WanGP wrapper around the official SCAIL-Pose preprocessing pipeline:
- DWpose (YOLOX+RTMPose) for 2D keypoints + bbox.
- NLFPose for 3D pose.
- Taichi cylinder renderer + 2D overlay.
"""
def __init__(self, gpu_id: int = 0, *, multi_person: bool = False, max_people: int = 2):
if not torch.cuda.is_available():
raise RuntimeError("SCAIL pose preprocessing requires CUDA (GPU-only).")
self.gpu_id = int(gpu_id)
self.multi_person = bool(multi_person)
self.max_people = max(1, int(max_people))
# Some upstream helpers use bare `.cuda()` / default device semantics.
torch.cuda.set_device(self.gpu_id)
self.detector = DWposeDetector(use_batch=False).to(self.gpu_id)
eager_ckpt = fl.locate_file("pose/nlf_l_multi_0.3.2.eager.safetensors")
yolox_onnx = fl.locate_file("pose/yolox_l.onnx")
self.model_nlf = load_multiperson_nlf_eager(
checkpoint_path=eager_ckpt,
yolox_onnx_path=yolox_onnx,
device=f'cuda:{self.gpu_id}'
)
# Lazy-loaded for multi-person mode.
self._sam_segmenter = None
self._matanyone_model = None
self._matanyone_version = None
def unload(self):
"""Release NLF model from memory (RAM and VRAM)."""
import gc
if hasattr(self, 'model_nlf') and self.model_nlf is not None:
# Move model to CPU first to free VRAM
self.model_nlf.to('cpu')
# Delete the model
del self.model_nlf
self.model_nlf = None
if hasattr(self, 'detector') and self.detector is not None:
self.detector.to('cpu')
del self.detector
self.detector = None
if getattr(self, "_sam_segmenter", None) is not None:
try:
self._sam_segmenter.model.to("cpu")
except Exception:
pass
self._sam_segmenter = None
if getattr(self, "_matanyone_model", None) is not None:
try:
self._matanyone_model.to("cpu")
except Exception:
pass
self._matanyone_model = None
self._matanyone_version = None
# Force garbage collection and empty CUDA cache
gc.collect()
torch.cuda.empty_cache()
def _ensure_sam_loaded(self) -> None:
if self._sam_segmenter is not None:
return
from preprocessing.matanyone.tools.base_segmenter import BaseSegmenter
# `BaseSegmenter` loads weights from `ckpts/mask/*safetensors` via `files_locator`.
self._sam_segmenter = BaseSegmenter(SAM_checkpoint=None, model_type="vit_h", device=f"cuda:{self.gpu_id}")
def _ensure_matanyone_loaded(self) -> None:
from preprocessing.matanyone.utils.model_assets import get_selected_matanyone_version, load_selected_matanyone_model
scail_matanyone_config = {"matanyone_version": "v1"}
selected_version = get_selected_matanyone_version(scail_matanyone_config)
if self._matanyone_model is not None and self._matanyone_version == selected_version:
return
if self._matanyone_model is not None:
self._matanyone_model.to("cpu")
self._matanyone_model = None
torch.cuda.empty_cache()
self._matanyone_model, self._matanyone_version, _ = load_selected_matanyone_model(scail_matanyone_config)
self._matanyone_model = self._matanyone_model.eval()
self._matanyone_model.to(f"cuda:{self.gpu_id}")
def _extract_and_render_multi(
self,
vr_frames_np: list[np.ndarray],
poses_list,
det_results_list,
) -> torch.Tensor:
if not vr_frames_np:
return torch.empty((0,), dtype=torch.float32)
first_bboxes = det_results_list[0] if det_results_list else None
if not first_bboxes:
return torch.empty((0,), dtype=torch.float32)
indices = get_largest_bbox_indices(first_bboxes, self.max_people)
if not indices:
return torch.empty((0,), dtype=torch.float32)
height, width = vr_frames_np[0].shape[:2]
pose0 = poses_list[0]
# 1) Build SAM prompts from DWpose (bbox + a few stable keypoints).
considered_points = {0, 1, 14, 15}
box_list_px: list[np.ndarray] = []
points_list_px: list[np.ndarray] = []
for idx in indices:
x1, y1, x2, y2 = first_bboxes[idx]
box_px = np.array([x1 * width, y1 * height, x2 * width, y2 * height], dtype=np.float32)
candidate = np.asarray(pose0["bodies"]["candidate"][idx], dtype=np.float32)
subset = np.asarray(pose0["bodies"]["subset"][idx])
subset_mod = subset.copy()
for k in range(len(subset_mod)):
if k not in considered_points:
subset_mod[k] = -1
pts = candidate[subset_mod != -1]
pts = np.asarray(pts, dtype=np.float32).reshape(-1, 2)
pts = pts[(pts[:, 0] >= 0.0) & (pts[:, 1] >= 0.0)]
if pts.shape[0] == 0:
pts = np.array([[(x1 + x2) * 0.5, (y1 + y2) * 0.5]], dtype=np.float32)
pts_px = pts.copy()
pts_px[:, 0] *= width
pts_px[:, 1] *= height
box_list_px.append(box_px)
points_list_px.append(pts_px)
# 2) Initial per-person masks on the first frame (SAM).
self._ensure_sam_loaded()
self._sam_segmenter.reset_image()
self._sam_segmenter.set_image(vr_frames_np[0])
init_masks: list[np.ndarray] = []
for box_px, pts_px in zip(box_list_px, points_list_px):
labels = np.ones((pts_px.shape[0],), dtype=np.int32)
masks, scores, _logits = self._sam_segmenter.predictor.predict(
point_coords=pts_px,
point_labels=labels,
box=box_px,
multimask_output=True,
)
if masks is None or len(masks) == 0:
init_masks.append(np.zeros((height, width), dtype=np.uint8))
continue
best = int(np.argmax(scores)) if scores is not None and len(scores) > 0 else 0
init_masks.append(masks[best].astype(np.uint8) * 255)
# Unload SAM to free VRAM before MatAnyone
if self._sam_segmenter is not None:
self._sam_segmenter.reset_image()
self._sam_segmenter.model.to("cpu")
del self._sam_segmenter
self._sam_segmenter = None
torch.cuda.empty_cache()
# 3) Track/segment each person across the video (MatAnyone).
self._ensure_matanyone_loaded()
from preprocessing.matanyone.matanyone.inference.inference_core import InferenceCore
from preprocessing.matanyone.matanyone_wrapper import matanyone as matanyone_run
vr_frames_list = []
for mask0 in init_masks:
processor = InferenceCore(self._matanyone_model, cfg=self._matanyone_model.cfg)
masked_frames, _alpha = matanyone_run(processor, vr_frames_np, mask0)
vr_frames_list.append(torch.from_numpy(np.stack(masked_frames, axis=0)))
del processor
# Unload MatAnyone to free VRAM before NLF
if self._matanyone_model is not None:
self._matanyone_model.to("cpu")
del self._matanyone_model
self._matanyone_model = None
self._matanyone_version = None
torch.cuda.empty_cache()
# 4) Run NLF per segmented person-video.
nlf_results = process_video_multi_nlf(self.model_nlf, vr_frames_list)
# 5) Limit DWpose overlays to the same number of persons for visualization.
poses_vis, det_vis = change_poses_to_limit_num(list(poses_list), list(det_results_list), num_bboxes=len(indices))
frames_np_rgba = render_multi_nlf_as_images(nlf_results, poses_vis, reshape_pool=None, intrinsic_matrix=None)
frames_rgb = np.stack([f[:, :, :3] for f in frames_np_rgba], axis=0).astype(np.float32)
frames_t = torch.from_numpy(frames_rgb).permute(0, 3, 1, 2) # T,C,H,W
frames_t = frames_t / 127.5 - 1.0
return frames_t.permute(1, 0, 2, 3).contiguous()
def extract_and_render(
self,
video_frames: torch.Tensor,
ref_image: Image.Image,
mask_frames: torch.Tensor | None = None,
align_pose: bool = True,
) -> torch.Tensor:
torch.cuda.set_device(self.gpu_id)
if video_frames is None or not isinstance(video_frames, torch.Tensor):
raise TypeError("video_frames must be a torch.Tensor")
if video_frames.dim() != 4:
raise ValueError(f"Expected video tensor (C,T,H,W), got {tuple(video_frames.shape)}")
if ref_image is None or not isinstance(ref_image, Image.Image):
raise TypeError("ref_image must be a PIL.Image")
out_w, out_h = ref_image.size
target_size = (int(out_w), int(out_h))
c, t, h, w = video_frames.shape
if t == 0:
return torch.empty((0,), dtype=torch.float32)
pil_frames: list[Image.Image] = []
vr_frames_np: list[np.ndarray] = []
for frame_idx in range(t):
frame_np = _tensor_frame_to_uint8_rgb(video_frames[:, frame_idx])
frame_pil = Image.fromarray(frame_np, mode="RGB")
# Output dimensions must match the ref image; also ensures internal pose extraction
# resolution is <= output resolution.
frame_pil = ImageOps.fit(frame_pil, target_size, method=_RESAMPLE_LANCZOS, centering=(0.5, 0.5))
if mask_frames is not None:
mask_pil = _tensor_mask_to_pil(mask_frames[:, frame_idx] if mask_frames.dim() == 4 else mask_frames[frame_idx])
mask_pil = ImageOps.fit(mask_pil, target_size, method=_RESAMPLE_NEAREST, centering=(0.5, 0.5))
mask_arr = np.array(mask_pil, dtype=np.float32) / 255.0
rgb_arr = np.array(frame_pil, dtype=np.float32)
rgb_arr = (rgb_arr * mask_arr[:, :, None]).clip(0, 255).astype(np.uint8)
frame_pil = Image.fromarray(rgb_arr, mode="RGB")
pil_frames.append(frame_pil)
vr_frames_np.append(np.array(frame_pil, dtype=np.uint8))
detector_return_list = [self.detector(pil_frame) for pil_frame in pil_frames]
poses, _scores, det_results = zip(*detector_return_list)
if self.multi_person:
return self._extract_and_render_multi(vr_frames_np, list(poses), list(det_results))
vr_frames = torch.from_numpy(np.stack(vr_frames_np, axis=0)) # (T,H,W,3) uint8
nlf_results = process_video_nlf(self.model_nlf, vr_frames, det_results)
# If nothing was detected across the whole clip, return empty tensor (caller handles it).
first_pose_idx = None
for i, item in enumerate(nlf_results):
if len(item.get("nlfpose", [])) == 0:
continue
first = item["nlfpose"][0]
if first is None:
continue
try:
has_any = len(first) > 0
except TypeError:
has_any = False
if has_any:
first_pose_idx = i
break
if first_pose_idx is None:
return torch.empty((0,), dtype=torch.float32)
target_H, target_W = out_h, out_w
ori_camera_pose = intrinsic_matrix_from_field_of_view([target_H, target_W])
ori_focal = ori_camera_pose[0, 0]
if align_pose:
# Reference image pose + NLF (single frame)
ref_rgb = ref_image.convert("RGB")
ref_rgb = ImageOps.fit(ref_rgb, target_size, method=_RESAMPLE_LANCZOS, centering=(0.5, 0.5))
pose_ref, _score_ref, det_result_ref = self.detector(ref_rgb)
if det_result_ref is None or len(det_result_ref) == 0:
align_pose = False
else:
vr_ref = torch.from_numpy(np.array(ref_rgb, dtype=np.uint8)).unsqueeze(0)
nlf_results_ref = process_video_nlf(self.model_nlf, vr_ref, [det_result_ref])
pose_3d_first_driving_frame = nlf_results[first_pose_idx]["nlfpose"][0][0].cpu().numpy()
pose_3d_coco_first_driving_frame = process_data_to_COCO_format(pose_3d_first_driving_frame)
poses_2d_ref = pose_ref["bodies"]["candidate"][0][:14]
poses_2d_ref[:, 0] = poses_2d_ref[:, 0] * target_W
poses_2d_ref[:, 1] = poses_2d_ref[:, 1] * target_H
poses_2d_subset = pose_ref["bodies"]["subset"][0][:14]
pose_3d_coco_first_driving_frame = pose_3d_coco_first_driving_frame[:14]
valid_upper_indices = []
valid_lower_indices = []
upper_body_indices = [0, 2, 3, 5, 6]
lower_body_indices = [9, 10, 12, 13]
for j in range(len(poses_2d_subset)):
if poses_2d_subset[j] != -1.0 and np.sum(pose_3d_coco_first_driving_frame[j]) != 0:
if j in upper_body_indices:
valid_upper_indices.append(j)
if j in lower_body_indices:
valid_lower_indices.append(j)
if len(valid_lower_indices) >= 4:
valid_indices = [1] + valid_lower_indices
new_camera_intrinsics, scale_m = solve_new_camera_params_down(
pose_3d_coco_first_driving_frame[valid_indices], ori_focal, [target_H, target_W], poses_2d_ref[valid_indices]
)
else:
valid_indices = [1] + valid_upper_indices
new_camera_intrinsics, scale_m = solve_new_camera_params_central(
pose_3d_coco_first_driving_frame[valid_indices], ori_focal, [target_H, target_W], poses_2d_ref[valid_indices]
)
poses_list = list(poses)
_ = scale_faces(poses_list, [pose_ref])
nlf_results = recollect_nlf(nlf_results)
poses_list = recollect_dwposes(poses_list)
shift_dwpose_according_to_nlf(
collect_smpl_poses(nlf_results), poses_list, ori_camera_pose, new_camera_intrinsics, target_H, target_W
)
frames_np_rgba = render_nlf_as_images(nlf_results, poses_list, reshape_pool=None, intrinsic_matrix=new_camera_intrinsics)
frames_rgb = np.stack([f[:, :, :3] for f in frames_np_rgba], axis=0).astype(np.float32)
frames_t = torch.from_numpy(frames_rgb).permute(0, 3, 1, 2) # T,C,H,W
frames_t = frames_t / 127.5 - 1.0
return frames_t.permute(1, 0, 2, 3).contiguous()
# Non-aligned path (official fallback)
nlf_results = recollect_nlf(nlf_results)
frames_np_rgba = render_nlf_as_images(nlf_results, list(poses), reshape_pool=None, intrinsic_matrix=ori_camera_pose)
frames_rgb = np.stack([f[:, :, :3] for f in frames_np_rgba], axis=0).astype(np.float32)
frames_t = torch.from_numpy(frames_rgb).permute(0, 3, 1, 2)
frames_t = frames_t / 127.5 - 1.0
return frames_t.permute(1, 0, 2, 3).contiguous()