File size: 9,935 Bytes
09462dc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
import argparse
import os
import os.path as osp
import shutil
import numpy as np
import torch
import gc
import sys
import cv2
from tqdm import tqdm
from PIL import Image
import decord
from decord import VideoReader, cpu
try:
    import moviepy.editor as mpy
except ImportError:
    import moviepy as mpy
import copy
import glob

# Add project root to sys.path
current_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(current_dir)
if project_root not in sys.path:
    sys.path.insert(0, project_root)

sys.path.append(os.path.join(project_root, "sam2"))
from sam2.build_sam import build_sam2_video_predictor

from DWPoseProcess.dwpose import DWposeDetector
from NLFPoseExtract.extract_nlfpose_batch import process_video_multi_nlf
from NLFPoseExtract.nlf_render import render_multi_nlf_as_images

def get_largest_bbox_indices(bboxes, num_bboxes=2):
    # 计算每个bbox的面积
    def calculate_area(bbox):
        x1, y1, x2, y2 = bbox
        return (x2 - x1) * (y2 - y1)
    
    # 计算每个bbox的面积,并保留原索引
    bboxes_with_area = [(i, calculate_area(bbox)) for i, bbox in enumerate(bboxes)]
    
    # 根据面积从大到小排序
    bboxes_with_area.sort(key=lambda x: x[1], reverse=True)
    
    # 取出面积最大的 num_bboxes 个索引
    largest_indices = [idx for idx, _ in bboxes_with_area[:num_bboxes]]
    
    return largest_indices

def change_poses_to_limit_num(poses, bboxes, num_bboxes=2):
    bboxes = list(bboxes)  # ✅ 转换为可变列表
    for idx, (pose, bbox) in enumerate(zip(poses, bboxes)):
        if len(bbox) == 0:
            continue
        largest_indices = get_largest_bbox_indices(bbox, num_bboxes)
        
        # 过滤 subset、hands、faces
        pose['bodies']['subset'] = pose['bodies']['subset'][largest_indices]
        
        new_hands = []
        for i in largest_indices:
            if 2*i+1 < len(pose['hands']):
                new_hands.append(pose['hands'][2*i])
                new_hands.append(pose['hands'][2*i+1])
        pose['hands'] = new_hands
        
        pose['faces'] = [pose['faces'][i] for i in largest_indices if i < len(pose['faces'])]
        
        bboxes[idx] = [bbox[i] for i in largest_indices]

    return poses, bboxes

def get_samurai_crop_video(video_input_path, video_output_root, bboxes_0, final_keypoints_list, predictor=None, use_green_background=True):
    decord.bridge.set_bridge("torch")
    # 用 decord 读取视频帧
    if video_input_path.endswith(".mp4"):
        vr = VideoReader(video_input_path)
        loaded_frames = vr.get_batch(list(range(len(vr)))).numpy()
        height, width = loaded_frames[0].shape[:2]

    # 每个人一个输出视频
    num_persons = len(final_keypoints_list)
    print(f"Detected {num_persons} persons, will save {num_persons} videos.")

    prompts = {fid: ((x1, y1, x2, y2), 0) for fid, (x1, y1, x2, y2) in enumerate(bboxes_0)}
    with torch.inference_mode(), torch.autocast("cuda", dtype=torch.float16):
        for person_idx in range(num_persons):
            print(f"Processing person {person_idx + 1}/{num_persons}...")

            state = predictor.init_state(video_input_path, offload_video_to_cpu=True)
            bbox, track_label = prompts[person_idx]
            bbox = (bbox[0] * width, bbox[1] * height, bbox[2] * width, bbox[3] * height)
            points = copy.deepcopy(final_keypoints_list[person_idx])
            points[:, 0] *= width
            points[:, 1] *= height
            
            _, _, masks = predictor.add_new_points_or_box(state, box=bbox, points=points, labels=np.ones(points.shape[0]), frame_idx=0, obj_id=0)

            output_frames = []
            output_mask_frames = []
            repeat_flag = False

            for frame_idx, object_ids, masks in predictor.propagate_in_video(state):
                img = loaded_frames[frame_idx].copy()
                for obj_id, mask in zip(object_ids, masks):
                    mask = mask[0].cpu().numpy() > 0.0   # 更新 mask
                    mask_log = np.zeros_like(img)
                    if use_green_background:
                        mask_img = np.full_like(img, (30, 60, 30))
                    else:
                        mask_img = np.zeros_like(img)
                    mask_img[mask] = img[mask]
                    mask_log[mask] = 255
                output_frames.append(mask_img)    # mask_img: array of [h, w, 3]
                output_mask_frames.append(mask_log)    # mask: array of [h, w]

            del state
            gc.collect()
            torch.cuda.empty_cache()

            # 用 moviepy 保存视频
            output_name = os.path.join(video_output_root, f"{person_idx+1}.mp4")
            clip = mpy.ImageSequenceClip(output_frames, fps=16)
            clip.write_videofile(output_name, codec="libx264", audio=False)
            print(f"Saved {output_name}")

    # del predictor # Do not delete predictor here as it might be reused or managed outside
    gc.collect()
    torch.clear_autocast_cache()
    torch.cuda.empty_cache()

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument('--subdir', type=str, required=True, help='Path to the subdirectory containing GT.mp4')
    parser.add_argument('--model_path', type=str, default='pretrained_weights/nlf_l_multi_0.3.2.torchscript', 
                    help='Path to NLF model')
    parser.add_argument('--resolution', type=int, nargs=2, default=[512, 512], help='Resolution [H, W]')
    args = parser.parse_args()

    subdir = args.subdir
    model_path = args.model_path
    resolution = args.resolution

    video_input_path = osp.join(subdir, "driving.mp4")
    if not osp.exists(video_input_path):
        print(f"Error: {video_input_path} does not exist.")

    # 1. Extract DWpose and BBoxes
    print("Extracting DWpose and BBoxes...")
    detector = DWposeDetector(use_batch=False).to(0)
    
    vr = VideoReader(video_input_path)
    vr_frames = vr.get_batch(list(range(len(vr)))).asnumpy() # T H W C
    
    # Resize if needed? process_pose.py does resize. 
    # But run_samurai_mp4.py seems to use original video for samurai.
    # process_multinlf_after_samurai.py uses samurai output which is same resolution as input?
    # Let's stick to original resolution for extraction to match run_samurai_mp4 logic which uses original video.
    
    detector_return_list = []
    pil_frames = []
    for i in tqdm(range(len(vr_frames)), desc="Detecting poses"):
        pil_frame = Image.fromarray(vr_frames[i])
        pil_frames.append(pil_frame)
        detector_result = detector(pil_frame)
        detector_return_list.append(detector_result)
    
    poses, scores, det_results = zip(*detector_return_list)
    # poses is tuple of dicts, det_results is tuple of lists of bboxes
    
    # Save meta if needed, or just use in memory. 
    # run_samurai_mp4.py saves to meta/keypoints.pt and meta/bboxes.pt
    meta_dir = osp.join(subdir, "meta")
    os.makedirs(meta_dir, exist_ok=True)
    torch.save(poses, osp.join(meta_dir, "keypoints.pt"))
    torch.save(det_results, osp.join(meta_dir, "bboxes.pt"))
    
    # 2. Run Samurai Segmentation
    print("Running Samurai Segmentation...")
    samurai_output_root = osp.join(subdir, "samurai")
    if osp.exists(samurai_output_root):
        shutil.rmtree(samurai_output_root)
    os.makedirs(samurai_output_root, exist_ok=True)
    
    device = "cuda:0"
    predictor = build_sam2_video_predictor("configs/sam2.1/sam2.1_hiera_l.yaml", "sam2/checkpoints/sam2.1_hiera_large.pt", device=device)
    
    # Prepare inputs for samurai
    bboxes_0 = det_results[0]
    indices = get_largest_bbox_indices(bboxes_0)
    bboxes_0 = [bboxes_0[index] for index in indices]
    
    keypoints_0 = poses[0]['bodies']['candidate']
    subset_0 = poses[0]['bodies']['subset']
    chosen_keypoints = keypoints_0[indices]
    
    final_keypoints_list = []
    for i in range(len(chosen_keypoints)):
        keypoints_for_person = chosen_keypoints[i]
        subset_for_person = subset_0[i]
        considered_points = [0, 1, 14, 15]
        # Create a copy to avoid modifying original if needed, though subset_for_person is from tensor
        subset_for_person_mod = subset_for_person.copy() 
        for k in range(len(subset_for_person_mod)):
            if k not in considered_points:
                subset_for_person_mod[k] = -1
        new_keypoints = keypoints_for_person[subset_for_person_mod != -1]
        final_keypoints_list.append(new_keypoints)
        
    get_samurai_crop_video(video_input_path, samurai_output_root, bboxes_0, final_keypoints_list, predictor=predictor)
    
    del predictor
    gc.collect()
    torch.cuda.empty_cache()
    
    # 3. Render Multi NLF
    print("Rendering Multi NLF...")
    model_nlf = torch.jit.load(model_path).cuda().eval()
    decord.bridge.set_bridge("torch")
    
    vr_frames_list = []
    for samurai_mp4_path in sorted(glob.glob(osp.join(samurai_output_root, '*.mp4'))):
        vr_tmp = VideoReader(samurai_mp4_path)
        vr_frames_tmp = vr_tmp.get_batch(list(range(len(vr_tmp))))
        vr_frames_list.append(vr_frames_tmp)
        
    # Filter poses for rendering
    # change_poses_to_limit_num modifies poses in-place or returns new ones?
    # It returns poses, bboxes. And it modifies the lists passed to it?
    # poses is a tuple from zip, convert to list
    poses_list = list(poses)
    det_results_list = list(det_results)
    
    poses_list, det_results_list = change_poses_to_limit_num(poses_list, det_results_list)
    
    nlf_results = process_video_multi_nlf(model_nlf, vr_frames_list)
    frames_ori_np = render_multi_nlf_as_images(nlf_results, poses_list, reshape_pool=None)
    
    out_path = osp.join(subdir, 'rendered.mp4')
    mpy.ImageSequenceClip(frames_ori_np, fps=16).write_videofile(out_path)
    print(f"Done! Output saved to: {out_path}")