Spaces:
Running on Zero
Running on Zero
File size: 10,041 Bytes
ccfee12 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 | import torch, os, sys, glob
import argparse
import math
import numpy as np
from PIL import Image
sys.path.append(os.getcwd())
sys.path.append("./DepthAnything3/src")
from DepthAnything3.src.depth_anything_3.api import DepthAnything3
from diffsynth.core import ModelConfig
from diffsynth import load_state_dict
from src.MetaView_pipeline import MetaViewPipeline
import torch.nn.functional as F
def compute_target_extrinsic(yaw_deg, pitch_deg, radius):
"""
Compute the camera extrinsic matrix (World-to-Camera) for rotation
around a sphere center in front of the camera.
Supports simultaneous yaw (left-right) and pitch (up-down) angles.
Args:
yaw_deg (float): Yaw angle in degrees.
pitch_deg (float): Pitch angle in degrees.
radius (float): Distance from the rotation sphere center to the camera
(typically the depth of the target object).
Returns:
numpy.ndarray: 4x4 extrinsic matrix.
"""
yaw = np.radians(yaw_deg)
pitch = np.radians(pitch_deg)
# Rotation matrix around Y axis (Yaw)
R_y = np.array([
[np.cos(yaw), 0, np.sin(yaw)],
[0, 1, 0 ],
[-np.sin(yaw), 0, np.cos(yaw)]
])
# Rotation matrix around X axis (Pitch)
R_x = np.array([
[1, 0, 0 ],
[0, np.cos(pitch), -np.sin(pitch)],
[0, np.sin(pitch), np.cos(pitch) ]
])
# Combined rotation (pitch first, then yaw)
R = R_y @ R_x
# Set sphere center coordinates C
C = np.array([0.0, 0.0, radius])
# Compute translation vector t = C - R * C
t = C - R @ C
# Construct 4x4 extrinsic matrix
T = np.eye(4)
T[:3, :3] = R
T[:3, 3] = t
return T
def main():
parser = argparse.ArgumentParser(description="MetaView Interactive Inference CLI")
# Core interactive parameters
parser.add_argument("--image_path", type=str, required=True, help="Path to the input image")
parser.add_argument("--output_path", type=str, default="./output_novel_view.png", help="Path to save the generated image")
parser.add_argument("--yaw", type=float, default=0.0, help="Yaw angle in degrees (e.g., 60 for right, -60 for left)")
parser.add_argument("--pitch", type=float, default=0.0, help="Pitch angle in degrees (e.g., 30 for top, -30 for bottom)")
parser.add_argument("--radius", type=float, default=None, help="Rotation radius. If None, auto-calculated from center depth.")
# Model path parameters
parser.add_argument("--da3_giant_path", type=str, default="../../Depth-Anything-3/model/DA3-GIANT-1.1", help="Path to DA3 Giant")
parser.add_argument("--da3_depth_path", type=str, default="../../Depth-Anything-3/model/DA3NESTED-GIANT-LARGE-1.1", help="Path to DA3 Depth model")
parser.add_argument("--qwen_path", type=str, default=None, help="Base path to Qwen-Image-Edit models")
parser.add_argument("--ckpt_path", type=str, required=True, help="Path to the trained MetaView checkpoint (.safetensors)")
args = parser.parse_args()
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# Global parameter configuration
export_3D_feat_layers = [19, 27, 33, 39]
prope_dim_arrange = [64, 20, 20, 24]
add_depth = (len(prope_dim_arrange) == 4)
merge_3D = True
prompt = ["镜头视角转到指定位置"]
# 1. Load input image
print(f"[*] Loading input image from {args.image_path}...")
original_image = Image.open(args.image_path).convert("RGB")
edit_image = original_image.resize((960, 528))
# =========================================================================
# 2. Depth and geometry prior extraction (Depth & Intrinsics)
# =========================================================================
print("[*] Loading DepthAnything3 Prior Models...")
with torch.inference_mode():
# Load feature extraction model (GIANT)
model_3D = DepthAnything3.from_pretrained(args.da3_giant_path).to(device=device)
print(" -> Extracting 3D Features and Intrinsics...")
feat_3D_output = model_3D.inference([edit_image], export_feat_layers=export_3D_feat_layers, process_res=840)
# Process intrinsics Ks
intri = feat_3D_output.intrinsics[0]
width = intri[0, 2] * 2
height = intri[1, 2] * 2
Ks_matrix = [
[intri[0, 0] / width, 0.0, 0.0],
[0.0, intri[1, 1] / height, 0.0],
[0.0, 0.0, 1.0],
]
Ks = torch.Tensor(Ks_matrix)
Ks = torch.stack([Ks, Ks], dim=0).unsqueeze(0) # Shape: (1, 2, 3, 3)
# Process features feat_3D
feats = [torch.from_numpy(feat_3D_output.aux[f"feat_layer_{layer}"]) for layer in export_3D_feat_layers]
feat_3D = torch.cat(feats, dim=-1).to(dtype=torch.bfloat16, device=device)
# Release feature extraction model
model_3D.to("cpu")
del model_3D
torch.cuda.empty_cache()
# Load depth extraction model (NESTED)
print(" -> Extracting Depth Map...")
model_depth = DepthAnything3.from_pretrained(args.da3_depth_path).to(device=device)
prediction = model_depth.inference([edit_image], process_res=840)
depth_edit = torch.Tensor(prediction.depth).unsqueeze(0)
depth_edit = F.interpolate(depth_edit, size=(528, 960), mode='bilinear', align_corners=False)[0]
depth_latent = torch.zeros_like(depth_edit)
depth = torch.cat([depth_latent, depth_edit], dim=0).unsqueeze(0) # Shape: (1, 2, H, W)
# Release depth model
model_depth.to("cpu")
del model_depth
torch.cuda.empty_cache()
# =========================================================================
# 3. Target pose calculation
# =========================================================================
# Auto-derive radius: if user did not specify radius, use center depth from the depth map
if args.radius is None:
depth_squeeze = depth[0, 1] # Get real depth channel
z_c = depth_squeeze[depth_squeeze.shape[0]//2, depth_squeeze.shape[1]//2].item()
args.radius = z_c
print(f"[*] Auto-calculated rotation radius from center depth: {args.radius:.4f}")
print(f"[*] Calculating Target Pose -> Yaw: {args.yaw}°, Pitch: {args.pitch}°, Radius: {args.radius}")
extrinsic_target = compute_target_extrinsic(args.yaw, args.pitch, args.radius)
extrinsic_source = np.eye(4)
# Construct viewmats tensor: Shape (1, 2, 4, 4) -> [Target, Source]
viewmats = torch.Tensor(np.stack((extrinsic_target, extrinsic_source), axis=0)).unsqueeze(0)
# =========================================================================
# 4. Generation model loading and inference (DiT Pipeline)
# =========================================================================
print("[*] Loading Qwen-Image-Edit Pipeline...")
if args.qwen_path:
print(f"[*] Loading Qwen-Image-Edit from {args.qwen_path}")
pipe = MetaViewPipeline.from_pretrained(
torch_dtype=torch.bfloat16,
device="cuda",
model_configs=[
ModelConfig(path=glob.glob(f"{args.qwen_path}/Qwen-Image-Edit/transformer/diffusion_pytorch_model*.safetensors")),
ModelConfig(path=glob.glob(f"{args.qwen_path}/Qwen-Image-Edit/text_encoder/model*.safetensors")),
ModelConfig(path=glob.glob(f"{args.qwen_path}/Qwen-Image-Edit/vae/diffusion_pytorch_model.safetensors")),
],
tokenizer_config=None,
processor_config=ModelConfig(path=f"{args.qwen_path}/Qwen-Image-Edit/processor/"),
)
else: # Auto download
pipe = MetaViewPipeline.from_pretrained(
torch_dtype=torch.bfloat16,
device="cuda",
model_configs=[
ModelConfig(model_id="Qwen/Qwen-Image-Edit", origin_file_pattern="transformer/diffusion_pytorch_model*.safetensors"),
ModelConfig(model_id="Qwen/Qwen-Image", origin_file_pattern="text_encoder/model*.safetensors"),
ModelConfig(model_id="Qwen/Qwen-Image", origin_file_pattern="vae/diffusion_pytorch_model.safetensors"),
],
tokenizer_config=None,
processor_config=ModelConfig(model_id="Qwen/Qwen-Image-Edit", origin_file_pattern="processor/"),
)
print(f"[*] Loading MetaView Weights from {args.ckpt_path}...")
state_dict = load_state_dict(args.ckpt_path)
pipe.dit.load_state_dict(state_dict, strict=False)
print("[*] Starting Generation (40 Steps)...")
with torch.inference_mode():
generated_image = pipe(
prompt, edit_image=edit_image, edit_image_auto_resize=False,
seed=0,
viewmats=viewmats.to(device=device, dtype=torch.bfloat16),
Ks=Ks.to(device=device, dtype=torch.bfloat16),
prope_dim_arrange=prope_dim_arrange,
add_attn=True,
add_3D=True,
feat_3D=feat_3D,
depth=depth.to(device=device, dtype=torch.bfloat16) if add_depth else None,
merge_3D=merge_3D,
val=True,
num_inference_steps=40,
height=528, width=960,
)
# =========================================================================
# 5. Save result (stitch source and generated images for comparison)
# =========================================================================
stitched_image = Image.new('RGB', (960 * 2, 528), (255, 255, 255))
stitched_image.paste(edit_image, (0, 0))
stitched_image.paste(generated_image, (960, 0))
os.makedirs(os.path.dirname(os.path.abspath(args.output_path)), exist_ok=True)
stitched_image.save(args.output_path)
print(f"Success! Result saved to {args.output_path}")
if __name__ == '__main__':
main() |