wangzhibo
2025-08-03 56d194a4300610a17cc3c6682aaf63f039e9c4ab
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
245
246
247
248
249
250
251
import av
import os
import pims
import numpy as np
from torch.utils.data import Dataset
from torchvision.transforms.functional import to_pil_image
from PIL import Image
import imageio.v3 as iio
import uuid
import traceback
import subprocess
import time
 
class VideoReader(Dataset):
    def __init__(self, path, transform=None):
        self.video = pims.PyAVVideoReader(path)
        self.rate = self.video.frame_rate
         
        # 直接拿第一帧的 shape(H, W, C)
        first_frame = np.asarray(self.video[0])
        self.height, self.width = first_frame.shape[:2]
 
        self.transform = transform
        
    @property
    def frame_rate(self):
        return self.rate
        
    def __len__(self):
        return len(self.video)
        
    def __getitem__(self, idx):
        frame = self.video[idx]
        frame = Image.fromarray(np.asarray(frame))
        if self.transform is not None:
            frame = self.transform(frame)
        return frame
 
 
class VideoWriter:
    def __init__(self, output_path, frame_rate, width,height, bit_rate=1000000,audio_source_path=None):
        """
        :param path: 输出文件路径
        :param frame_rate: 视频帧率
        :param bit_rate: 视频码率
        :param audio_source_path: 原视频文件路径(用于拷贝音频)
        """
        print(f"ready to create video file : {output_path}")
        self.output_path = output_path
        self.audio_source_path =audio_source_path
        if self.audio_source_path and os.path.exists(self.audio_source_path):
            # using temp video file without audio
            self.need_audio_action = True
            self.temp_path = f"{uuid.uuid4()}.mp4"
            print(self.temp_path)
            self.container = av.open(self.temp_path, mode='w')
        else:
            self.container = av.open(self.output_path, mode='w')
            self.need_audio_action = False
 
        self.v_stream = self.container.add_stream('h264', rate=round(frame_rate))
        self.v_stream.pix_fmt = 'yuv420p'
        self.v_stream.bit_rate = bit_rate
        self.v_stream.width = width
        self.v_stream.height = height
        self.v_stream.codec_context.options = {
            'crf': '18',  # 控制画质,越小越清晰(18是高质量)
            'preset': 'veryfast',     # 影响编码速度,medium 是1x,fast是2x faster是3x veryfast是4x superfast是6x ultrafast是10x,文件体积大约会增加(以medium为基准) 10%,15%,20% 30% 50%
            'tune': 'film',
        }
        '''
        self.audio_in_container = None
        self.audio_stream_in = None
        self.audio_out_stream = None
 
        if self.audio_source_path:
            try:
                self.audio_in_container = av.open(audio_source_path)
                if self.audio_in_container.streams.audio:
                    self.audio_stream_in = self.audio_in_container.streams.audio[0]
            except:
                pass
 
        if self.audio_stream_in:
            self.audio_out_stream = self.container.add_stream(self.audio_stream_in.codec.name)
        else:
            self.audio_out_stream = None
        '''
 
        
    def write(self, frames):
       
        if frames.size(1) == 1:
            frames = frames.repeat(1, 3, 1, 1)
 
        frames = frames.mul(255).byte().cpu().permute(0, 2, 3, 1).numpy()
 
        for t in range(frames.shape[0]):
            frame = frames[t]
            video_frame = av.VideoFrame.from_ndarray(frame, format='rgb24')
            for packet in self.v_stream.encode(video_frame):
                self.container.mux(packet)
    
    def write_nparray(self,frames):
 
        try:
            if frames.ndim == 3:  # 单帧
                frames = np.expand_dims(frames, axis=0)
 
            if frames.shape[-1] == 1:  # 灰度图
                frames = np.repeat(frames, 3, axis=-1)  # HWC → RGB
 
            for t in range(frames.shape[0]):
                frame = frames[t]
                # 直接用numpy写入
                video_frame = av.VideoFrame.from_ndarray(frame, format='rgb24')
                for packet in self.v_stream.encode(video_frame):
                    self.container.mux(packet)
        except Exception as e:
            print(f"{e}")
            traceback.format_exc() # 打印堆栈信息(如果 logger 支持 debug)
    
 
 
    def merge_video_audio(self,video_path, audio_path, output_path):
        """
        用 FFmpeg 把 video_path 的画面 和 audio_path 的音频 合并成 output_path
        最短的一个结束 (-shortest),合并后删除 video_path
        """
        try:
            # 构造 FFmpeg 命令
            cmd = [
                "ffmpeg", "-y",                # -y 覆盖输出
                "-loglevel", "error",       # 只显示错误
                "-i", video_path,              # 画面
                "-i", audio_path,              # 音频
                "-c:v", "copy",                 # 视频不重新编码
                "-c:a", "copy",                 # 音频不重新编码
                "-map", "0:v:0",                # 第一个输入的视频
                "-map", "1:a:0",                # 第二个输入的音频
                "-shortest",                    # 以最短的输入结束
                output_path
            ]
 
            print(f"[INFO] 开始合并视频和音频...\n{' '.join(cmd)}")
            subprocess.run(cmd, check=True)
 
            print("[INFO] 合并完成,等待 1 秒后删除原画面视频...")
            time.sleep(1)
 
            # 删除原视频
            if os.path.exists(video_path):
                os.remove(video_path)
                print(f"[INFO] 已删除原画面视频: {video_path}")
 
        except subprocess.CalledProcessError as e:
            print(f"[ERROR] FFmpeg 合并失败,错误码: {e.returncode}")
        except Exception as e:
            print(f"[ERROR] 发生未知错误: {e}")
 
 
                
    def close(self):   
        '''     
        if self.audio_stream_in and self.audio_out_stream:
            for packet in self.audio_in_container.demux(self.audio_stream_in):
                if packet.dts is None:
                    continue
                packet.stream = self.audio_out_stream  # 直接改,不用 copy()
                self.container.mux(packet)
        if self.audio_in_container:
            self.audio_in_container.close()
        '''
        self.container.close()
 
        if self.need_audio_action:
            self.merge_video_audio(self.temp_path,self.audio_source_path,self.output_path)
 
      
 
class WebMWriter:
    """
    支持透明通道的中间视频格式
    # 示例使用:
    writer = WebMWriter("output_alpha.webm", fps=30)
    for i in range(frames):
        writer.write(frame)
    writer.close()
    """
    def __init__(self, filename, fps=30):
        self.filename = filename
        self.fps = fps
        self.frames = []
 
    def write(self, frame: np.ndarray):
        """
        传入的 frame 应该是含 alpha 通道的 numpy 数组,shape=(H,W,4),dtype=uint8
        RGBA 格式,alpha 通道决定透明度
        """
        assert frame.shape[2] == 4, "Frame must have 4 channels (RGBA)"
        self.frames.append(frame)
 
    def close(self):
        iio.imwrite(
            self.filename,
            self.frames,
            fps=self.fps,
            codec="vp8",               # vp8支持alpha通道
            ffmpeg_params=[
                "-auto-alt-ref", "0",  # 关闭参考帧,支持透明
                "-pix_fmt", "yuva420p" # VP8透明格式
            ],
            quality=10  # 质量,范围1-63,10比较高质量
        )
        print(f"WebM video saved to {self.filename}")
 
 
class ImageSequenceReader(Dataset):
    def __init__(self, path, transform=None):
        self.path = path
        self.files = sorted(os.listdir(path))
        self.transform = transform
        
    def __len__(self):
        return len(self.files)
    
    def __getitem__(self, idx):
        with Image.open(os.path.join(self.path, self.files[idx])) as img:
            img.load()
        if self.transform is not None:
            return self.transform(img)
        return img
 
 
class ImageSequenceWriter:
    def __init__(self, path, extension='jpg'):
        self.path = path
        self.extension = extension
        self.counter = 0
        os.makedirs(path, exist_ok=True)
    
    def write(self, frames):
        # frames: [T, C, H, W]
        for t in range(frames.shape[0]):
            to_pil_image(frames[t]).save(os.path.join(
                self.path, str(self.counter).zfill(4) + '.' + self.extension))
            self.counter += 1
            
    def close(self):
        pass