| | |
| | | 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 |
| | |
| | | |
| | | |
| | | class VideoWriter: |
| | | def __init__(self, path, frame_rate, bit_rate=1000000,audio_source_path=None): |
| | | 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: 原视频文件路径(用于拷贝音频) |
| | | """ |
| | | self.container = av.open(path, mode='w') |
| | | 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_source_path =audio_source_path |
| | | ''' |
| | | self.audio_in_container = None |
| | | self.audio_stream_in = None |
| | | self.audio_out_stream = None |
| | |
| | | self.audio_out_stream = self.container.add_stream(self.audio_stream_in.codec.name) |
| | | else: |
| | | self.audio_out_stream = None |
| | | ''' |
| | | |
| | | |
| | | |
| | | def write(self, frames): |
| | | # frames: [T, C, H, W] |
| | | self.v_stream.width = frames.size(3) |
| | | self.v_stream.height = frames.size(2) |
| | | |
| | | if frames.size(1) == 1: |
| | | frames = frames.repeat(1, 3, 1, 1) # convert grayscale to RGB |
| | | 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] |
| | | frame = av.VideoFrame.from_ndarray(frame, format='rgb24') |
| | | # old version |
| | | #self.container.mux(self.v_stream.encode(frame)) |
| | | for packet in self.v_stream.encode(frame): |
| | | video_frame = av.VideoFrame.from_ndarray(frame, format='rgb24') |
| | | for packet in self.v_stream.encode(video_frame): |
| | | self.container.mux(packet) |
| | | |
| | | def close(self): |
| | | # old version |
| | | #self.container.mux(self.v_stream.encode()) |
| | | for packet in self.v_stream.encode(): |
| | | 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) |
| | | |
| | | self.container.close() |
| | | 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): |