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