| import os |
| import shutil |
| import site |
| import sys |
| import importlib |
|
|
| def migrate_precompiled_packages(): |
| |
| target_site_packages = site.getsitepackages()[0] |
| |
| |
| packages_to_check = { |
| 'pytorch3d': ['pytorch3d', 'pytorch3d-0.7.8.dist-info'], |
| 'chumpy': ['chumpy', 'chumpy-0.70.dist-info'] |
| } |
| |
| print(f"📦 Starting migration check in: {os.getcwd()}") |
| |
| for pkg_name, folders in packages_to_check.items(): |
| |
| try: |
| importlib.import_module(pkg_name) |
| print(f"✅ {pkg_name} is already available. Skipping.") |
| continue |
| except ImportError: |
| print(f"🔍 {pkg_name} not found. Preparing to migrate...") |
|
|
| for folder in folders: |
| src = os.path.abspath(folder) |
| dst = os.path.join(target_site_packages, folder) |
| |
| if os.path.exists(src): |
| try: |
| if os.path.exists(dst): |
| print(f"⚠️ Removing existing {dst}...") |
| shutil.rmtree(dst) if os.path.isdir(dst) else os.remove(dst) |
| |
| print(f"🚚 Copying {folder} to site-packages...") |
| shutil.copytree(src, dst) |
| except Exception as e: |
| print(f"❌ Failed to migrate {folder}: {e}") |
| else: |
| print(f"❓ Source {folder} missing in root directory.") |
|
|
| |
| importlib.invalidate_caches() |
| if target_site_packages not in sys.path: |
| sys.path.insert(0, target_site_packages) |
|
|
| |
| try: |
| import torch |
| torch_lib_path = os.path.join(os.path.dirname(torch.__file__), "lib") |
| os.environ["LD_LIBRARY_PATH"] = torch_lib_path + ":" + os.environ.get("LD_LIBRARY_PATH", "") |
| print(f"🔗 LD_LIBRARY_PATH updated with torch libs.") |
| except Exception as e: |
| print(f"⚠️ Failed to set LD_LIBRARY_PATH: {e}") |
|
|
| |
| migrate_precompiled_packages() |
|
|
| |
| try: |
| import chumpy |
| import pytorch3d |
| from pytorch3d import _C |
| print(f"🎉 All systems go! PyTorch3D GPU: {hasattr(_C, 'rasterize_meshes')}") |
| except Exception as e: |
| print(f"🚨 Validation failed: {e}") |
| |
|
|
| import gradio as gr |
| import os |
| import json |
| import numpy as np |
| import cv2 |
| import base64 |
| import time |
| import shutil |
| import glob |
| from pathlib import Path |
| from typing import List, Tuple, Union |
| from concurrent.futures import ThreadPoolExecutor |
| import atexit |
| import uuid |
| import logging |
| from huggingface_hub import hf_hub_download |
| import torch |
| import decord |
| try: |
| import spaces |
| except ImportError: |
| |
| def space(func): |
| return func |
| import imageio |
|
|
| from einops import rearrange |
|
|
| from models.modules.ehm import EHM_v2 |
| from models.pipeline.ehm_pipeline import Ehm_Pipeline |
| from utils.pipeline_utils import to_tensor |
| from utils.graphics_utils import GS_Camera |
| from models.modules.renderer.body_renderer import Renderer2 as BodyRenderer |
| from pytorch3d.renderer import PointLights |
| from utils.general_utils import ( |
| ConfigDict, |
| rtqdm, |
| device_parser, |
| add_extra_cfgs, |
| ) |
| from scipy.signal import savgol_filter |
| try: |
| import spaces |
| except ImportError: |
| |
| def spaces(func): |
| return func |
|
|
|
|
| logger = logging.getLogger(__name__) |
|
|
| |
| |
| |
| TORCH_DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu") |
| ORT_PROVIDERS = ["CUDAExecutionProvider", "CPUExecutionProvider"] |
|
|
|
|
|
|
| meta_cfg = ConfigDict( |
| model_config_path=os.path.join("configs", "infer.yaml") |
| ) |
| meta_cfg = add_extra_cfgs(meta_cfg) |
|
|
| |
| body_renderer = BodyRenderer("assets/SMPLX", 1024, focal_length=24.0) |
|
|
|
|
| repo_id = "BestWJH/PEAR_models" |
| filename = "ehm_model_stage1.pt" |
|
|
| ehm_basemodel = hf_hub_download(repo_id=repo_id, filename=filename, repo_type="model") |
| ehm_model = Ehm_Pipeline(meta_cfg) |
|
|
| _state = torch.load(ehm_basemodel, map_location='cpu', weights_only=True) |
| ehm_model.backbone.load_state_dict(_state['backbone'], strict=False) |
| ehm_model.head.load_state_dict(_state['head'], strict=False) |
|
|
| ehm = EHM_v2("assets/FLAME", "assets/SMPLX", "assets/MANO") |
|
|
|
|
| |
|
|
| |
| def get_lights(device): |
| return PointLights(device=device, location=[[0.0, -1.0, -10.0]]) |
|
|
|
|
|
|
|
|
| |
| thread_pool_executor = ThreadPoolExecutor(max_workers=2) |
|
|
| def build_cameras_kwargs(batch_size, focal_length): |
| screen_size = ( |
| torch.tensor([1024, 1024], device=TORCH_DEVICE) |
| .float()[None] |
| .repeat(batch_size, 1) |
| ) |
| cameras_kwargs = { |
| "principal_point": torch.zeros(batch_size, 2, device=TORCH_DEVICE).float(), |
| "focal_length": focal_length, |
| "image_size": screen_size, |
| "device": TORCH_DEVICE, |
| } |
| return cameras_kwargs |
|
|
|
|
|
|
| def pad_and_resize(img, target_size=512): |
|
|
| h, w = img.shape[:2] |
|
|
| |
| scale = min(target_size / h, target_size / w) |
| new_w, new_h = int(w * scale), int(h * scale) |
| resized_img = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_LINEAR) |
|
|
| |
| padded_img = np.zeros((target_size, target_size, 3), dtype=np.uint8) |
| x_offset = (target_size - new_w) // 2 |
| y_offset = (target_size - new_h) // 2 |
| padded_img[y_offset:y_offset + new_h, x_offset:x_offset + new_w] = resized_img |
|
|
| return padded_img |
|
|
| def delete_later(path: Union[str, os.PathLike], delay: int = 600): |
| """Delete file or directory after specified delay (default 10 minutes)""" |
| def _delete(): |
| try: |
| if os.path.isfile(path): |
| os.remove(path) |
| elif os.path.isdir(path): |
| shutil.rmtree(path) |
| except Exception as e: |
| logger.warning(f"Failed to delete {path}: {e}") |
| |
| def _wait_and_delete(): |
| time.sleep(delay) |
| _delete() |
| |
| thread_pool_executor.submit(_wait_and_delete) |
| atexit.register(_delete) |
|
|
|
|
| def create_user_temp_dir(): |
| """Create a unique temporary directory for each user session""" |
| session_id = str(uuid.uuid4())[:8] |
| PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__)) |
| temp_dir = os.path.join( |
| PROJECT_ROOT, "temp_local", f"session_{session_id}" |
| ) |
|
|
| os.makedirs(temp_dir, exist_ok=True) |
| |
| |
| delete_later(temp_dir, delay=600) |
| |
| return temp_dir |
|
|
| def get_video_name(video_path): |
| """Extract video name without extension""" |
| return os.path.splitext(os.path.basename(video_path))[0] |
|
|
| def extract_first_frame(video_path): |
| """Extract first frame from video file""" |
| try: |
| cap = cv2.VideoCapture(video_path) |
| ret, frame = cap.read() |
| cap.release() |
| |
| if ret: |
| frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) |
| return frame_rgb |
| else: |
| return None |
| except Exception as e: |
| print(f"Error extracting first frame: {e}") |
| return None |
|
|
| def numpy_to_base64(arr): |
| """Convert numpy array to base64 string""" |
| return base64.b64encode(arr.tobytes()).decode('utf-8') |
|
|
|
|
| def polynomial_smooth(sequence, window_size=5, polyorder=2): |
|
|
| seq = np.asarray(sequence.cpu()) |
| if seq.ndim < 2: |
| raise ValueError(f"输入必须至少是 2 维,当前 shape={seq.shape}") |
|
|
| if window_size % 2 == 0: |
| raise ValueError("window_size 必须是奇数") |
| if polyorder >= window_size: |
| raise ValueError("polyorder 必须小于 window_size") |
|
|
| |
| smoothed = savgol_filter(seq, window_length=window_size, polyorder=polyorder, axis=0, mode='interp') |
| return smoothed |
|
|
|
|
| def handle_video_upload(video): |
| """Handle video upload and extract first frame""" |
| if video is None: |
| return None |
| |
| |
| user_temp_dir = create_user_temp_dir() |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| input_source = video if isinstance(video, str) else video.name |
|
|
| video_name = get_video_name(input_source) |
| video_path = os.path.join(user_temp_dir, f"{video_name}.mp4") |
|
|
| try: |
| |
| reader = imageio.get_reader(input_source) |
| meta_data = reader.get_meta_data() |
| fps = meta_data.get('fps', 30) |
| max_frames = int(fps * 3) |
| writer = imageio.get_writer(video_path, fps=fps, codec='libx264', quality=8) |
| |
| for i, frame in enumerate(reader): |
| if i >= max_frames: |
| break |
| writer.append_data(frame) |
| |
| reader.close() |
| writer.close() |
| print(f"成功截取前 3 秒视频并保存至: {video_path}") |
|
|
| except Exception as e: |
| print(f"imageio 处理视频失败: {e}") |
|
|
|
|
| print(f"📁 Video saved to: {video_path}") |
| |
| |
| frame = extract_first_frame(video_path) |
| if frame is None: |
| return None |
| |
| |
| h, w = frame.shape[:2] |
| scale = 336 / min(h, w) |
| new_h, new_w = int(h * scale)//2*2, int(w * scale)//2*2 |
| frame = cv2.resize(frame, (new_w, new_h), interpolation=cv2.INTER_LINEAR) |
| |
| |
| frame_data = { |
| 'data': numpy_to_base64(frame), |
| 'shape': frame.shape, |
| 'dtype': str(frame.dtype), |
| 'temp_dir': user_temp_dir, |
| 'video_name': video_name, |
| 'video_path': video_path |
| } |
| |
| |
| print(f"🎬 Video path: '{video}' -> Video name: '{video_name}'") |
|
|
| return (json.dumps(frame_data) ) |
|
|
| @spaces.GPU |
| @torch.no_grad() |
| def mesh_inference(temp_dir, video_name): |
|
|
| global body_renderer, ehm_model, ehm |
| body_renderer = body_renderer.to(TORCH_DEVICE) |
| ehm_model = ehm_model.to(TORCH_DEVICE) |
| ehm = ehm.to(TORCH_DEVICE) |
| lights = get_lights(TORCH_DEVICE) |
|
|
| |
| |
| video_path = os.path.join(temp_dir, f"{video_name}.mp4") |
|
|
| out_dir = os.path.join(temp_dir, "results") |
| os.makedirs(out_dir, exist_ok=True) |
| |
| |
| video_reader = decord.VideoReader(video_path) |
| |
|
|
|
|
| |
| print(f"🎯 Running EHM pipeline...") |
| ehm_out_dir = os.path.join(out_dir, "ehm_results") |
| os.makedirs(ehm_out_dir, exist_ok=True) |
|
|
|
|
| |
| all_meshes_img = [] |
| vertices_list = [] |
| body_sequence = [] |
| flame_sequence = [] |
| cam_sequence = [] |
| |
| for i in range(len(video_reader)): |
| frame = video_reader[i].asnumpy() |
| |
| resized = pad_and_resize(frame, target_size=256) |
| img_patch = to_tensor(resized, TORCH_DEVICE) |
| img_patch = torch.permute(img_patch/255,(2,0,1)).unsqueeze(0) |
|
|
| outputs = ehm_model(img_patch) |
|
|
|
|
| body_sequence.append(outputs['body_param']) |
| flame_sequence.append(outputs['flame_param']) |
| cam_sequence.append(outputs['pd_cam']) |
|
|
|
|
| fields1 = [ |
| "global_pose", "body_pose", "left_hand_pose", "right_hand_pose", |
| "hand_scale", "head_scale", "exp", "shape" |
| ] |
|
|
| processed1 = {} |
| for key in fields1: |
| data_list = [seq[key] for seq in body_sequence] |
| data_tensor = torch.cat(data_list, dim=0) |
| processed1[key] = torch.tensor(polynomial_smooth(data_tensor, window_size=7, polyorder=2)).cuda() |
|
|
| global_pose = processed1["global_pose"] |
| body_pose = processed1["body_pose"] |
| left_hand_pose = processed1["left_hand_pose"] |
| right_hand_pose = processed1["right_hand_pose"] |
| hand_scale = processed1["hand_scale"] |
| head_scale = processed1["head_scale"] |
| exp = processed1["exp"] |
| shape = processed1["shape"] |
|
|
| |
| fields2 = [ |
| "eye_pose_params", "pose_params", "jaw_params", |
| "eyelid_params", "expression_params", "shape_params" |
| ] |
|
|
| processed2 = {} |
| for key in fields2: |
| data_list = [seq[key] for seq in flame_sequence] |
| data_tensor = torch.cat(data_list, dim=0) |
| processed2[key] = torch.tensor(polynomial_smooth(data_tensor, window_size=5, polyorder=2)).cuda() |
|
|
| eye_pose_params = processed2["eye_pose_params"] |
| pose_params = processed2["pose_params"] |
| jaw_params = processed2["jaw_params"] |
| eyelid_params = processed2["eyelid_params"] |
| expression_params = processed2["expression_params"] |
| shape_params = processed2["shape_params"] |
|
|
|
|
| cam_sequence = torch.cat(cam_sequence, dim=0) |
| cam_sequence = torch.tensor(polynomial_smooth(cam_sequence, window_size=7, polyorder=2)).cuda() |
|
|
|
|
|
|
|
|
| for idx in range(global_pose.shape[0]): |
| |
| pd_cam = cam_sequence[idx:idx+1] |
|
|
| body_dict = { |
| "global_pose": global_pose[idx:idx+1], |
| "body_pose" : body_pose[idx:idx+1], |
| "left_hand_pose" : left_hand_pose[idx:idx+1], |
| "right_hand_pose" : right_hand_pose[idx:idx+1], |
| "hand_scale" : hand_scale[idx:idx+1], |
| "head_scale" : head_scale[idx:idx+1], |
| "exp" : exp[idx:idx+1] , |
| "shape": shape[idx:idx+1], |
| 'eye_pose' :None, |
| 'jaw_pose' : None, |
| 'joints_offset' : None |
| } |
| flame_dict ={ |
| "eye_pose_params" : eye_pose_params[idx:idx+1], |
| "pose_params" : pose_params[idx:idx+1], |
| "jaw_params" : jaw_params[idx:idx+1], |
| "eyelid_params" : eyelid_params[idx:idx+1], |
| "expression_params" : expression_params[idx:idx+1] , |
| "shape_params": shape_params[idx:idx+1] |
| } |
|
|
| pd_smplx_dict = ehm(body_dict, flame_dict, pose_type='aa') |
| pd_camera = GS_Camera(**build_cameras_kwargs(1,24), R = pd_cam[0:1,:3,:3], T = pd_cam[0:1,:3,3]) |
| pd_mesh_img = body_renderer.render_mesh(pd_smplx_dict['vertices'][None, 0,...], pd_camera, lights=lights,) |
| pd_mesh_img = (pd_mesh_img[:,:3].detach().cpu().numpy()).clip(0, 255).astype(np.uint8)[0].transpose(1,2,0) |
| |
| all_meshes_img.append(pd_mesh_img) |
|
|
| |
|
|
|
|
|
|
| |
| mesh_video_path = os.path.join(out_dir, "mesh_video.mp4") |
| |
| |
| |
| if len(all_meshes_img) == 0: |
| raise RuntimeError("No frames generated for mesh video.") |
| fps = 30 |
| try: |
| writer = imageio.get_writer( |
| mesh_video_path, |
| fps=fps, |
| codec="libx264", |
| pixelformat="yuv420p", |
| ffmpeg_params=["-movflags", "faststart"], |
| macro_block_size=None, |
| ) |
| for img in all_meshes_img: |
| |
| h, w = img.shape[:2] |
| img2 = img[: h - (h % 2), : w - (w % 2)] |
| writer.append_data(img2) |
| writer.close() |
| except Exception as e: |
| print(f"⚠️ imageio ffmpeg writer failed ({e}); falling back to mimwrite") |
| imageio.mimwrite(mesh_video_path, all_meshes_img, fps=fps) |
|
|
| |
| |
| faces = body_renderer.faces[0].detach().cpu().numpy() |
| vertices = np.stack(vertices_list, axis=0) if len(vertices_list) > 0 else np.empty((0, 0, 3), dtype=np.float32) |
| np.savez_compressed(os.path.join(out_dir, "results.npz"), vertices=vertices, faces=faces) |
|
|
|
|
| print(f"✅ EHM processing completed.") |
|
|
|
|
|
|
| |
| def launch_viz(original_image_state): |
| """Launch visualization with user-specific temp directory""" |
| if original_image_state is None: |
| return None, None |
|
|
| try: |
| |
| frame_data = json.loads(original_image_state) |
|
|
| temp_dir = frame_data.get("temp_dir", "temp_local") |
| video_name = frame_data.get("video_name", "video") |
|
|
| print(f"🚀 Starting recover for video: {video_name}") |
|
|
| |
| print("🎯 Running...") |
| out_dir = os.path.join(temp_dir, "results") |
| os.makedirs(out_dir, exist_ok=True) |
|
|
| mesh_inference(temp_dir, video_name) |
|
|
| delete_later(temp_dir, delay=600) |
|
|
| npz_path = os.path.join(out_dir, "results.npz") |
| mesh_video = os.path.join(out_dir, "mesh_video.mp4") |
|
|
| if os.path.exists(mesh_video): |
| print("✅ Tracking completed successfully!") |
| |
| return mesh_video, (npz_path if os.path.exists(npz_path) else None) |
| else: |
| print("❌ Tracking failed - no results generated") |
| return None, None |
|
|
| except Exception as e: |
| print(f"❌ Error in launch_viz: {e}") |
| return None, None |
|
|
|
|
|
|
| |
| print("🎨 Creating Gradio interface...") |
|
|
| with gr.Blocks( |
| theme=gr.themes.Soft(), |
| title="🎯 [PEAR](https://wujh2001.github.io/PEAR/)", |
| css=""" |
| .gradio-container { |
| max-width: 1200px !important; |
| margin: auto !important; |
| } |
| .gr-button { |
| margin: 5px; |
| } |
| .gr-form { |
| background: white; |
| border-radius: 10px; |
| padding: 20px; |
| box-shadow: 0 2px 10px rgba(0,0,0,0.1); |
| } |
| /* 移除 gr.Group 的默认灰色背景 */ |
| .gr-form { |
| background: transparent !important; |
| border: none !important; |
| box-shadow: none !important; |
| padding: 0 !important; |
| } |
| /* 固定3D可视化器尺寸(纯白卡片样式) */ |
| #viz_container { |
| height: 650px !important; |
| min-height: 650px !important; |
| max-height: 650px !important; |
| width: 100% !important; |
| margin: 0 !important; |
| padding: 12px !important; |
| overflow: hidden !important; |
| box-sizing: border-box !important; |
| border-radius: 14px !important; |
| border: 1px solid rgba(148, 163, 184, 0.6) !important; |
| background: #ffffff !important; |
| box-shadow: 0 10px 24px rgba(15, 23, 42, 0.12) !important; |
| display: flex !important; |
| align-items: center !important; |
| justify-content: center !important; |
| } |
| /* 固定“左侧上传”视频组件高度(不要影响右侧结果视频) */ |
| #video_input { |
| height: 300px !important; |
| min-height: 300px !important; |
| max-height: 300px !important; |
| } |
| #video_input video { |
| height: 260px !important; |
| max-height: 260px !important; |
| object-fit: contain !important; |
| background: #f8f9fa; |
| } |
| #video_input .gr-video-player { |
| height: 260px !important; |
| max-height: 260px !important; |
| } |
| |
| /* 强力移除examples的灰色背景 - 使用更通用的选择器 */ |
| .horizontal-examples, |
| .horizontal-examples > *, |
| .horizontal-examples * { |
| background: transparent !important; |
| background-color: transparent !important; |
| border: none !important; |
| } |
| |
| /* Examples组件水平滚动样式 */ |
| .horizontal-examples [data-testid="examples"] { |
| background: transparent !important; |
| background-color: transparent !important; |
| } |
| |
| .horizontal-examples [data-testid="examples"] > div { |
| background: transparent !important; |
| background-color: transparent !important; |
| overflow-x: auto !important; |
| overflow-y: hidden !important; |
| scrollbar-width: thin; |
| scrollbar-color: #667eea transparent; |
| padding: 0 !important; |
| margin-top: 10px; |
| border: none !important; |
| } |
| |
| .horizontal-examples [data-testid="examples"] table { |
| display: flex !important; |
| flex-wrap: nowrap !important; |
| min-width: max-content !important; |
| gap: 15px !important; |
| padding: 10px 0; |
| background: transparent !important; |
| border: none !important; |
| } |
| |
| .horizontal-examples [data-testid="examples"] tbody { |
| display: flex !important; |
| flex-direction: row !important; |
| flex-wrap: nowrap !important; |
| gap: 15px !important; |
| background: transparent !important; |
| } |
| |
| .horizontal-examples [data-testid="examples"] tr { |
| display: flex !important; |
| flex-direction: column !important; |
| min-width: 160px !important; |
| max-width: 160px !important; |
| margin: 0 !important; |
| background: white !important; |
| border-radius: 12px; |
| box-shadow: 0 3px 12px rgba(0,0,0,0.12); |
| transition: all 0.3s ease; |
| cursor: pointer; |
| overflow: hidden; |
| border: none !important; |
| } |
| |
| .horizontal-examples [data-testid="examples"] tr:hover { |
| transform: translateY(-4px); |
| box-shadow: 0 8px 20px rgba(102, 126, 234, 0.25); |
| } |
| |
| .horizontal-examples [data-testid="examples"] td { |
| text-align: center !important; |
| padding: 0 !important; |
| border: none !important; |
| background: transparent !important; |
| } |
| |
| .horizontal-examples [data-testid="examples"] td:first-child { |
| padding: 0 !important; |
| background: transparent !important; |
| } |
| |
| .horizontal-examples [data-testid="examples"] video { |
| border-radius: 8px 8px 0 0 !important; |
| width: 100% !important; |
| height: 90px !important; |
| object-fit: cover !important; |
| background: #f8f9fa !important; |
| } |
| |
| .horizontal-examples [data-testid="examples"] td:last-child { |
| font-size: 11px !important; |
| font-weight: 600 !important; |
| color: #333 !important; |
| padding: 8px 12px !important; |
| background: linear-gradient(135deg, #f8f9ff 0%, #e6f3ff 100%) !important; |
| border-radius: 0 0 8px 8px; |
| } |
| |
| /* 滚动条样式 */ |
| .horizontal-examples [data-testid="examples"] > div::-webkit-scrollbar { |
| height: 8px; |
| } |
| .horizontal-examples [data-testid="examples"] > div::-webkit-scrollbar-track { |
| background: transparent; |
| border-radius: 4px; |
| } |
| .horizontal-examples [data-testid="examples"] > div::-webkit-scrollbar-thumb { |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| border-radius: 4px; |
| } |
| .horizontal-examples [data-testid="examples"] > div::-webkit-scrollbar-thumb:hover { |
| background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%); |
| } |
| """ |
| ) as demo: |
| |
| |
| |
| gr.Markdown(""" |
| # ✨ PEAR |
| |
| Welcome to [PEAR](https://wujh2001.github.io/PEAR/)! This interface allows you to human mesh from a single image. |
| Please cite our paper and give us a star 🌟 if you find this project useful! |
| |
| **⚡ Quick Start:** Upload video → Click "Start Tracking Now!" |
| |
| **🔬 Advanced Usage with PEAR:** |
| |
| """) |
| |
| |
| status_indicator = gr.Markdown("**Reminder:** 🟢 This app currently only supports single human-centered video inputs (up to 3 seconds). For other input types, users can customize the application manually. In addition, the model used here is not our final version (Used in our paper and project page); the complete model will be officially released in a few months.") |
| |
| |
| with gr.Row(): |
| with gr.Column(scale=1): |
| |
| gr.Markdown("### 📂 Select Video") |
| |
| |
| video_input = gr.Video( |
| label="Upload Video or Select Example", |
| format="mp4", |
| height=250, |
| elem_id="video_input" |
| ) |
| |
|
|
| |
| gr.Markdown("🎨**Examples:** (scroll horizontally to see all videos)") |
| with gr.Row(elem_classes=["horizontal-examples"]): |
| |
| |
| gr.Examples( |
| examples=[ |
| ["example/example_1.mp4"], |
| ["example/example_2.mp4"], |
| ], |
| inputs=[video_input], |
| outputs=[video_input], |
| fn=None, |
| cache_examples=False, |
| label="", |
| examples_per_page=6 |
| ) |
| |
| with gr.Column(scale=2): |
| |
| with gr.Group(): |
| gr.Markdown("### ✨ Human Mesh Visualization") |
| |
| viz_video = gr.Video( |
| label="Reconstructed Human Mesh Video", |
| height=650, |
| elem_id="viz_container", |
| interactive=False, |
| autoplay=False, |
| sources=None, |
| ) |
|
|
| |
| with gr.Row(): |
| with gr.Column(scale=3): |
| launch_btn = gr.Button("🚀 Start Tracking Now!", variant="primary", size="lg") |
| with gr.Column(scale=1): |
| clear_all_btn = gr.Button("🗑️ Clear All", variant="secondary", size="sm") |
|
|
| |
| with gr.Row(): |
| with gr.Column(): |
| parameters_download = gr.File( |
| label="📄 Download Mesh Results (vertices and faces)", |
| interactive=False, |
| ) |
|
|
|
|
| |
| gr.HTML(""" |
| <div style='background: linear-gradient(135deg, #e8eaff 0%, #f0f2ff 100%); |
| border-radius: 8px; padding: 20px; margin: 15px 0; |
| box-shadow: 0 2px 8px rgba(102, 126, 234, 0.1); |
| border: 1px solid rgba(102, 126, 234, 0.15);'> |
| <div style='text-align: center;'> |
| <h3 style='color: #4a5568; margin: 0 0 10px 0; font-size: 18px; font-weight: 600;'> |
| ⭐ Love PEAR? Give us a Star! ⭐ |
| </h3> |
| <p style='color: #666; margin: 0 0 15px 0; font-size: 14px; line-height: 1.5;'> |
| Help us grow by starring our repository on GitHub! Your support means a lot to the community. 🚀 |
| </p> |
| <a href="https://wujh2001.github.io/PEAR/" target="_blank" |
| style='display: inline-flex; align-items: center; gap: 8px; |
| background: rgba(102, 126, 234, 0.1); color: #4a5568; |
| padding: 10px 20px; border-radius: 25px; text-decoration: none; |
| font-weight: bold; font-size: 14px; border: 1px solid rgba(102, 126, 234, 0.2); |
| transition: all 0.3s ease;' |
| onmouseover="this.style.background='rgba(102, 126, 234, 0.15)'; this.style.transform='translateY(-2px)'" |
| onmouseout="this.style.background='rgba(102, 126, 234, 0.1)'; this.style.transform='translateY(0)'"> |
| <span style='font-size: 16px;'>⭐</span> |
| Star PEAR on GitHub |
| </a> |
| </div> |
| </div> |
| """) |
|
|
| |
| |
| gr.HTML(""" |
| <div style='text-align: center; margin: 20px 0 10px 0;'> |
| <span style='font-size: 12px; color: #888; font-style: italic;'> |
| Powered by PEAR | Built with ❤️ for the Computer Vision Community |
| </span> |
| </div> |
| """) |
|
|
|
|
| |
| original_image_state = gr.State(None) |
|
|
|
|
|
|
| |
| video_input.change( |
| fn=handle_video_upload, |
| inputs=[video_input], |
| outputs=[original_image_state], |
| api_name=False |
| ) |
|
|
| launch_btn.click( |
| fn=launch_viz, |
| inputs=[original_image_state], |
| outputs=[viz_video, parameters_download], |
| api_name=False |
| ) |
|
|
|
|
| |
| if __name__ == "__main__": |
| print("🌟 Launching PEAR Local Version...") |
| print("🔗 Running in Local Processing Mode") |
| demo.queue().launch() |
| |
|
|