| 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: |
| 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)) |
|
|
| |
| 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}' |
| ) |
|
|
| |
| 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: |
| |
| self.model_nlf.to('cpu') |
| |
| 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 |
|
|
| |
| 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 |
|
|
| |
| 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] |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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() |
|
|
| |
| 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 |
|
|
| |
| 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() |
|
|
| |
| nlf_results = process_video_multi_nlf(self.model_nlf, vr_frames_list) |
|
|
| |
| 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) |
| 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") |
|
|
| |
| |
| 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)) |
| nlf_results = process_video_nlf(self.model_nlf, vr_frames, det_results) |
|
|
| |
| 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: |
| |
| 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) |
| frames_t = frames_t / 127.5 - 1.0 |
| return frames_t.permute(1, 0, 2, 3).contiguous() |
|
|
| |
| 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() |
|
|