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()