在python中优雅的使用ffmpeg:PyAV

ffmpeg是强大的多媒体处理工具,堪称多媒体处理的瑞士军刀,涵盖了大量的多媒体处理工具。但是ffmpeg是由纯C语言写成,对于python用户来说使用难度较高,为此今天向大家推荐一款在python中使用ffmpeg的开发包:PyAV

PyAV提供了ffmpeg的python接口,但实际是它只是使用ffmpeg做后端,使用Cython封装了ffmpeg的接口,所以实际调用的还是ffmpeg。

PyAV安装

PyAV是跨平台的,可以根据自己的环境和平台选择安装。

Windows安装PyAV:

在Windows下安装PyAV可以参照博客https://blog.youkuaiyun.com/Dillon2015/article/details/91358179

Mac OS X和Ubuntu上安装PyAV:

在Mac OS X和Ubuntu上安装PyAV可以参考官网安装方法

PyAV使用

PyAV提供了非常方便的接口使开发者不需要太关注底层细节。

视频分割为独立的帧

有的时候做处理时需要将一段视频按帧分成一张张图像,在ffmpeg命令行中只需要一条命令:

ffmpeg –i test.avi –r 1 –f image2 image-%3d.jpeg

-r表示每秒提取图像的数量,如果等于帧率则会将所有帧都提取出来。

在PyAV中实现同样的功能也很简单,

import av
​
container = av.open(path_to_video)
#path_to_video是你视频的路径
for frame in container.decode(video=0):
    frame.to_image().save('frame-%04d.jpg' % frame.index)

保存关键帧

对于一个视频序列来说并不是所有帧都一样,因为视频编码在进行帧间预测时会出现相互参考的情况,如果一帧的参考帧丢失或损坏了那么这一帧就无法正确解码,所以对于那些用于被参考的帧就相对更重要了。

av.video.frame.VideoFrame类中有一个属性key_frame用以表示该帧是否是关键帧。

import av
import av.datasets
​
container = av.open(path_to_video)
# Signal that we only want to look at keyframes.
stream = container.streams.video[0]
stream.codec_context.skip_frame = 'NONKEY'
​
for frame in container.decode(stream):
    # We use `frame.pts` as `frame.index` won't make must sense with the `skip_frame`.
    frame.to_image().save(
        'night-sky.{:04d}.jpg'.format(frame.pts),
        quality=80,
    )

在以上代码中跳过了非关键帧,将所有关键帧保存下来。

视频转封装

视频转封装就是改变视频的封装格式而不改变其中视频流、音频流等的编码方式,例如从mp4->mkv

过程如下:

 

import av
import av.datasets
​
input_ = av.open(path_to_video)
output = av.open('remuxed.mkv', 'w')
​
# Make an output stream using the input as a template. This copies the stream
# setup from one to the other.
in_stream = input_.streams.video[0]
out_stream = output.add_stream(template=in_stream)
​
for packet in input_.demux(in_stream):
    # We need to skip the "flushing" packets that `demux` generates.
    if packet.dts is None:
        continue
​
    # We need to assign the packet to the new stream.
    packet.stream = out_stream
    output.mux(packet)
​
output.close()

生成视频

PyAV还可以和numpy配合使用,直接将ndarray转换成视频帧,使得对帧的操作更加灵活和方便。

from __future__ import division
​
import numpy as np
​
import av
​
duration = 4
fps = 24
total_frames = duration * fps
container = av.open('test.mp4', mode='w')
stream = container.add_stream('mpeg4', rate=fps)
stream.width = 480
stream.height = 320
stream.pix_fmt = 'yuv420p'
​
for frame_i in range(total_frames):
    img = np.empty((480, 320, 3))
    img[:, :, 0] = 0.5 + 0.5 * np.sin(2 * np.pi * (0 / 3 + frame_i / total_frames))
    img[:, :, 1] = 0.5 + 0.5 * np.sin(2 * np.pi * (1 / 3 + frame_i / total_frames))
    img[:, :, 2] = 0.5 + 0.5 * np.sin(2 * np.pi * (2 / 3 + frame_i / total_frames))
​
    img = np.round(255 * img).astype(np.uint8)
    img = np.clip(img, 0, 255)
​
    frame = av.VideoFrame.from_ndarray(img, format='rgb24')
    for packet in stream.encode(frame):
        container.mux(packet)
​
#Flush stream
for packet in stream.encode():
    container.mux(packet)
​
#Close the file
container.close()

以上代码生成了一段480x320帧率24fps的视频。

小结

PyAV还要更多更强大的功能,感兴趣的小伙伴可以自己安装试试哦。

感兴趣的请关注微信公众号Video Coding

 

import os import cv2 import numpy as np import psutil import time import argparse import json from datetime import datetime import logging import signal import sys import traceback import threading import GPUtil import subprocess import gc import shutil import queue import concurrent.futures import tracemalloc import platform import requests import zipfile class VideoProcessor: def __init__(self, config): self.config = config self.canceled = False self.start_time = time.time() self.frame_counter = 0 self.progress = 0 self.status = "就绪" self.fps = 0.0 self.mem_usage = 0.0 self.cpu_percent = 0.0 self.system_mem_percent = 0.0 self.system_mem_used = 0.0 self.system_mem_available = 0.0 self.gpu_load = 0.0 self.gpu_memory_used = 0.0 self.gpu_memory_total = 0.0 self.logger = logging.getLogger("VideoProcessor") self.resources = [] # 跟踪需要释放的资源 self.monitor_active = False self.monitor_thread = None # 多线程队列 self.frame_queue = queue.Queue(maxsize=self.config.get('queue_size', 30)) self.processed_queue = queue.Queue(maxsize=self.config.get('queue_size', 30)) # CUDA流管理 self.cuda_streams = [] self.cuda_ctx = None # 检测移动环境 self.is_mobile = self.detect_mobile_environment() if self.is_mobile: self.logger.info("检测到移动环境,启用移动端优化配置") # 内存跟踪 if self.config.get('enable_memory_monitor', False): tracemalloc.start() self.logger.info("内存跟踪已启用") # 注册信号处理 signal.signal(signal.SIGINT, self.signal_handler) signal.signal(signal.SIGTERM, self.signal_handler) def detect_mobile_environment(self): """检测是否在移动环境中运行""" try: system = platform.system().lower() uname = os.uname() # Android检测 if 'linux' in system and 'android' in uname.version.lower(): self.logger.info("检测到Android环境") return True # iOS检测 if system == 'darwin' and 'ios' in uname.machine.lower(): self.logger.info("检测到iOS环境") return True return False except Exception as e: self.logger.warning(f"移动环境检测失败: {str(e)}") return False def signal_handler(self, signum, frame): """处理中断信号""" self.logger.warning(f"接收到中断信号: {signum}, 正在优雅地停止...") self.cancel() sys.exit(1) def start_resource_monitor(self, interval=1): """启动资源监控线程""" self.monitor_active = True self.monitor_thread = threading.Thread( target=self.monitor_resources, args=(interval,), daemon=True ) self.monitor_thread.start() self.logger.info("资源监控线程已启动") def stop_resource_monitor(self): """停止资源监控线程""" if self.monitor_thread and self.monitor_thread.is_alive(): self.monitor_active = False self.monitor_thread.join(timeout=2.0) self.logger.info("资源监控线程已停止") def monitor_resources(self, interval=1): """资源监控线程函数""" self.logger.info("资源监控开始") print("\n资源监控 | 时间戳 | CPU使用率 | 内存使用 | GPU使用率 | GPU显存") print("-" * 70) while self.monitor_active: try: # CPU监控 cpu_percent = psutil.cpu_percent(interval=None) # 内存监控 mem = psutil.virtual_memory() mem_usage = f"{mem.used / (1024**3):.1f}GB/{mem.total / (1024**3):.1f}GB" # GPU监控 gpu_info = "" try: gpus = GPUtil.getGPUs() if gpus: gpu = gpus[0] gpu_info = f"{gpu.load*100:.1f}% | {gpu.memoryUsed:.1f}MB/{gpu.memoryTotal:.0f}MB" # 更新GPU状态 self.gpu_load = gpu.load * 100 self.gpu_memory_used = gpu.memoryUsed self.gpu_memory_total = gpu.memoryTotal else: gpu_info = "No GPU" except Exception as e: gpu_info = f"Error: {str(e)}" timestamp = time.strftime('%H:%M:%S') print(f"{timestamp} | {cpu_percent:6.1f}% | {mem_usage:^15} | {gpu_info}") self.logger.info(f"资源监控 | {timestamp} | CPU: {cpu_percent}% | 内存: {mem_usage} | GPU: {gpu_info}") # 内存泄漏检测 if self.config.get('enable_memory_monitor', False): snapshot = tracemalloc.take_snapshot() top_stats = snapshot.statistics('lineno') self.logger.info("内存分配Top 10:") for stat in top_stats[:10]: self.logger.info(str(stat)) time.sleep(interval) except Exception as e: self.logger.error(f"资源监控出错: {str(e)}") time.sleep(5) # 出错后等待5秒再重试 def init_cuda(self): """初始化CUDA环境""" if not self.config.get('use_gpu_processing', False) or self.is_mobile: return try: device_id = self.config.get('gpu_device_index', 0) if cv2.cuda.getCudaEnabledDeviceCount() > device_id: # 设置CUDA设备 cv2.cuda.setDevice(device_id) device = cv2.cuda.DeviceInfo(device_id) self.logger.info(f"使用GPU设备: {device.name()}") # 创建CUDA流 num_streams = self.config.get('cuda_streams', 4) self.cuda_streams = [cv2.cuda_Stream() for _ in range(num_streams)] self.logger.info(f"已创建 {num_streams} 个CUDA流") # 创建CUDA上下文 self.cuda_ctx = cv2.cuda.Device(device_id).createContext() self.logger.info("CUDA上下文已创建") else: self.logger.warning("请求的GPU设备不可用,将使用CPU处理") self.config['use_gpu_processing'] = False except Exception as e: self.logger.error(f"CUDA初始化失败: {str(e)}") self.config['use_gpu_processing'] = False def open_video_with_acceleration(self, file_path): """使用硬件加速打开视频""" # 移动端使用专用API if self.is_mobile: self.logger.info("移动端: 使用Android专用API") try: # Android专用API cap = cv2.VideoCapture(file_path, cv2.CAP_ANDROID) if cap.isOpened(): self.logger.info("Android专用API打开成功") self.resources.append(cap) return cap else: self.logger.warning("Android专用API打开失败,尝试默认方式") except: self.logger.warning("Android专用API不可用,使用默认方式") # 桌面端或移动端备选方案 if self.config.get('hardware_acceleration', 'disable') == 'disable': cap = cv2.VideoCapture(file_path) self.resources.append(cap) return cap cap = cv2.VideoCapture() self.resources.append(cap) acceleration = { 'auto': cv2.VIDEO_ACCELERATION_ANY, 'any': cv2.VIDEO_ACCELERATION_ANY, 'nvidia': cv2.VIDEO_ACCELERATION_NVIDIA, 'intel': cv2.VIDEO_ACCELERATION_INTEL, 'vaapi': cv2.VIDEO_ACCELERATION_VAAPI }.get(self.config.get('hardware_acceleration', 'auto'), cv2.VIDEO_ACCELERATION_ANY) params = [ cv2.CAP_PROP_HW_ACCELERATION, acceleration, cv2.CAP_PROP_HW_DEVICE, self.config.get('gpu_device_index', 0) ] # 降低延迟的优化参数 if self.config.get('reduce_latency', True): params.extend([ cv2.CAP_PROP_BUFFERSIZE, self.config.get('buffer_size', 3), cv2.CAP_PROP_FPS, self.config.get('target_fps', 30) ]) # MJPEG压缩 if self.config.get('use_mjpeg', True): params.extend([ cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('M','J','P','G') ]) # 设置解码线程数 decoding_threads = self.config.get('decoding_threads', 0) if decoding_threads > 0: params.extend([cv2.CAP_PROP_FFMPEG_THREADS, decoding_threads]) try: cap.open(file_path, apiPreference=cv2.CAP_FFMPEG, params=params) # Intel专用加速 if self.config.get('hardware_acceleration', '') == 'intel' and cap.isOpened(): cap.set(cv2.CAP_PROP_INTEL_VIDEO_SRC_HW_ACCEL, 1) except Exception as e: self.logger.error(f"硬件加速打开失败: {str(e)}, 使用默认方式") cap = cv2.VideoCapture(file_path) return cap def update_system_stats(self): """更新系统资源统计""" self.cpu_percent = psutil.cpu_percent(interval=0.1) mem = psutil.virtual_memory() self.system_mem_percent = mem.percent self.system_mem_used = mem.used / (1024 ** 3) # GB self.system_mem_available = mem.available / (1024 ** 3) # GB def print_progress(self): """美观的进度显示""" elapsed = time.time() - self.start_time eta = (100 - self.progress) * elapsed / max(1, self.progress) if self.progress > 0 else 0 # 进度条 bar_length = 30 filled_length = int(bar_length * self.progress / 100) bar = '█' * filled_length + '-' * (bar_length - filled_length) # 队列状态 queue_status = f"Q: {self.frame_queue.qsize()}/{self.processed_queue.qsize()}" progress_str = ( f"进度: |{bar}| {self.progress}% " f"| 速度: {self.fps:.1f}fps " f"| 用时: {elapsed:.1f}s " f"| 剩余: {eta:.1f}s " f"| CPU: {self.cpu_percent:.0f}% " f"| 内存: {self.mem_usage:.1f}MB " f"| GPU: {self.gpu_load:.1f}% " f"| {queue_status}" ) print(f"\r{progress_str}", end="") self.logger.info(progress_str) def capture_thread(self, cap, total_frames): """视频捕获线程 (生产者)""" frame_idx = 0 while cap.isOpened() and not self.canceled and frame_idx < total_frames: ret, frame = cap.read() if not ret: break # 放入队列,非阻塞方式防止死锁 try: self.frame_queue.put((frame_idx, frame), timeout=1.0) frame_idx += 1 except queue.Full: if self.canceled: break time.sleep(0.01) # 发送结束信号 self.frame_queue.put((None, None)) self.logger.info(f"捕获线程完成,共捕获 {frame_idx} 帧") def processing_thread(self, output_resolution): """视频处理线程 (消费者)""" output_width, output_height = output_resolution while not self.canceled: try: # 获取帧,带超时防止死锁 frame_idx, frame = self.frame_queue.get(timeout=2.0) # 结束信号 if frame_idx is None: self.processed_queue.put((None, None)) self.frame_queue.task_done() break # 处理帧 try: # 移动端使用轻量级算法 if self.is_mobile: # 移动端优化:使用Canny边缘检测替代复杂特征检测 gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) edges = cv2.Canny(gray, 100, 200) # 将边缘检测结果与原帧合并 frame[:, :, 0] = np.minimum(frame[:, :, 0] + edges, 255) frame[:, :, 1] = np.minimum(frame[:, :, 1] + edges, 255) frame[:, :, 2] = np.minimum(frame[:, :, 2] + edges, 255) # 移动端使用快速插值方法 processed_frame = cv2.resize(frame, output_resolution, interpolation=cv2.INTER_LINEAR) else: # 桌面端使用完整算法 if self.config.get('use_gpu_processing', False) and self.cuda_streams: # 选择CUDA流 (轮询) stream_idx = frame_idx % len(self.cuda_streams) stream = self.cuda_streams[stream_idx] # 上传到GPU gpu_frame = cv2.cuda_GpuMat() gpu_frame.upload(frame, stream=stream) # GPU处理 if output_resolution: gpu_frame = cv2.cuda.resize(gpu_frame, output_resolution, stream=stream) # 下载回CPU processed_frame = gpu_frame.download(stream=stream) else: # CPU处理 if output_resolution: processed_frame = cv2.resize(frame, output_resolution) else: processed_frame = frame # 放入已处理队列 self.processed_queue.put((frame_idx, processed_frame), timeout=1.0) except cv2.error as e: if 'CUDA' in str(e): self.logger.error(f"GPU处理失败: {str(e)},切换到CPU模式") self.config['use_gpu_processing'] = False # 使用CPU重试 processed_frame = cv2.resize(frame, output_resolution) if output_resolution else frame self.processed_queue.put((frame_idx, processed_frame), timeout=1.0) else: self.logger.error(f"处理帧 {frame_idx} 失败: {str(e)}") except Exception as e: self.logger.error(f"处理帧 {frame_idx} 时出错: {str(e)}") self.frame_queue.task_done() except queue.Empty: if self.canceled: break except Exception as e: self.logger.error(f"处理线程出错: {str(e)}") self.logger.info("处理线程退出") def writer_thread(self, out, total_frames): """写入线程""" frame_idx = 0 last_log_time = time.time() while not self.canceled and frame_idx < total_frames: try: # 获取已处理帧 idx, processed_frame = self.processed_queue.get(timeout=2.0) # 结束信号 if idx is None: break # 写入输出 if processed_frame is not None: out.write(processed_frame) # 更新计数 self.frame_counter += 1 frame_idx += 1 # 计算帧率 elapsed = time.time() - self.start_time self.fps = self.frame_counter / elapsed if elapsed > 0 else 0 # 更新内存使用 process = psutil.Process(os.getpid()) self.mem_usage = process.memory_info().rss / (1024 ** 2) # MB # 更新系统状态 self.update_system_stats() # 更新进度 self.progress = int(frame_idx * 100 / total_frames) # 定期打印进度 current_time = time.time() if current_time - last_log_time > 1.0 or frame_idx % 50 == 0: self.print_progress() last_log_time = current_time # 内存管理 if frame_idx % 100 == 0: gc.collect() # 检查内存使用情况 if self.system_mem_percent > 90: self.logger.warning(f"系统内存使用超过90%! (当前: {self.system_mem_percent}%)") print(f"\n警告: 系统内存使用过高 ({self.system_mem_percent}%)") self.processed_queue.task_done() except queue.Empty: if self.canceled: break except Exception as e: self.logger.error(f"写入线程出错: {str(e)}") self.logger.info(f"写入线程完成,共写入 {frame_idx} 帧") def run(self): try: self.status = "处理中..." self.logger.info("视频处理开始") self.logger.info(f"主视频: {self.config['main_video']}") self.logger.info(f"副视频: {self.config['sub_video']}") self.logger.info(f"输出文件: {self.config['output_path']}") self.start_time = time.time() # 初始化CUDA self.init_cuda() # 启动资源监控 self.start_resource_monitor() # 打开主视频 self.logger.info("正在打开主视频...") main_cap = self.open_video_with_acceleration(self.config['main_video']) if not main_cap.isOpened(): self.status = "无法打开主视频文件!" self.logger.error(f"无法打开主视频文件: {self.config['main_video']}") return False # 获取主视频信息 main_fps = main_cap.get(cv2.CAP_PROP_FPS) main_width = int(main_cap.get(cv2.CAP_PROP_FRAME_WIDTH)) main_height = int(main_cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) main_total_frames = int(main_cap.get(cv2.CAP_PROP_FRAME_COUNT)) self.logger.info(f"主视频信息: {main_width}x{main_height}@{main_fps:.1f}fps, 总帧数: {main_total_frames}") # 打开副视频 self.logger.info("正在打开副视频...") sub_cap = self.open_video_with_acceleration(self.config['sub_video']) if not sub_cap.isOpened(): self.status = "无法打开副视频文件!" self.logger.error(f"无法打开副视频文件: {self.config['sub_video']}") return False # 获取副视频信息 sub_fps = sub_cap.get(cv2.CAP_PROP_FPS) sub_width = int(sub_cap.get(cv2.CAP_PROP_FRAME_WIDTH)) sub_height = int(sub_cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) sub_total_frames = int(sub_cap.get(cv2.CAP_PROP_FRAME_COUNT)) self.logger.info(f"副视频信息: {sub_width}x{sub_height}@{sub_fps:.1f}fps, 总帧数: {sub_total_frames}") # 创建输出目录 output_dir = os.path.dirname(self.config['output_path']) if output_dir and not os.path.exists(output_dir): try: os.makedirs(output_dir) self.logger.info(f"已创建输出目录: {output_dir}") except Exception as e: self.status = f"无法创建输出目录: {output_dir}" self.logger.error(f"创建输出目录失败: {str(e)}") return False # 创建输出视频 output_width, output_height = self.config['output_resolution'] fourcc = cv2.VideoWriter_fourcc(*'mp4v') out = cv2.VideoWriter(self.config['output_path'], fourcc, main_fps, (output_width, output_height)) self.resources.append(out) if not out.isOpened(): self.status = "无法创建输出视频文件!请检查分辨率设置。" self.logger.error(f"无法创建输出视频: {self.config['output_path']}, 分辨率: {output_width}x{output_height}") return False # 计算主视频分段参数 if self.config['main_segment_type'] == '秒': segment_length_main = int(float(self.config['segment_a']) * main_fps) else: segment_length_main = int(self.config['segment_a']) b1 = int(self.config['b1']) b2 = int(self.config['b2']) replace_frame_count = b2 - b1 + 1 # 计算副视频分段参数 if self.config['sub_segment_type'] == '秒': segment_length_sub = int(float(self.config['segment_c']) * sub_fps) else: segment_length_sub = int(self.config['segment_c']) d = int(self.config['d']) # 计算主视频段数 segments_main = (main_total_frames + segment_length_main - 1) // segment_length_main # 计算副视频段数 segments_sub = (sub_total_frames + segment_length_sub - 1) // segment_length_sub # 检查段数是否匹配 if segments_main > segments_sub: if self.config['sub_option'] == '循环使用': self.logger.warning(f"副视频段数不足({segments_sub}),将循环使用以满足主视频段数({segments_main})") else: self.status = "副视频段数不足,无法完成替换!" self.logger.error(f"副视频段数不足: {segments_sub} < {segments_main}") return False # 初始化性能监控 process = psutil.Process(os.getpid()) self.logger.info("="*50) self.logger.info("开始视频处理") self.logger.info(f"主视频: {self.config['main_video']} ({main_total_frames}帧, {main_fps:.1f}fps)") self.logger.info(f"副视频: {self.config['sub_video']} ({sub_total_frames}帧, {sub_fps:.1f}fps)") self.logger.info(f"输出文件: {self.config['output_path']}") self.logger.info(f"分辨率: {output_width}x{output_height}") self.logger.info(f"主视频分段数: {segments_main}, 每段{segment_length_main}帧") self.logger.info(f"替换帧范围: {b1}-{b2} (每段替换{replace_frame_count}帧)") self.logger.info(f"副视频分段数: {segments_sub}, 每段{segment_length_sub}帧") self.logger.info(f"副视频起始帧: {d}") self.logger.info(f"使用GPU处理: {self.config.get('use_gpu_processing', False)}") self.logger.info(f"CUDA流数量: {len(self.cuda_streams)}") self.logger.info(f"移动环境: {self.is_mobile}") self.logger.info("="*50) print("\n" + "="*50) print("开始视频处理") print(f"主视频: {self.config['main_video']} ({main_total_frames}帧, {main_fps:.1f}fps)") print(f"副视频: {self.config['sub_video']} ({sub_total_frames}帧, {sub_fps:.1f}fps)") print(f"输出文件: {self.config['output_path']}") print(f"分辨率: {output_width}x{output_height}") print(f"主视频分段数: {segments_main}, 每段{segment_length_main}帧") print(f"替换帧范围: {b1}-{b2} (每段替换{replace_frame_count}帧)") print(f"副视频分段数: {segments_sub}, 每段{segment_length_sub}帧") print(f"副视频起始帧: {d}") print(f"使用GPU处理: {self.config.get('use_gpu_processing', False)}") print(f"CUDA流数量: {len(self.cuda_streams)}") print(f"移动环境: {self.is_mobile}") print("="*50 + "\n") # 启动多线程处理 with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor: # 启动捕获线程 capture_future = executor.submit( self.capture_thread, main_cap, main_total_frames ) # 启动处理线程 processing_future = executor.submit( self.processing_thread, (output_width, output_height) ) # 启动写入线程 writer_future = executor.submit( self.writer_thread, out, main_total_frames ) # 等待所有线程完成 concurrent.futures.wait( [capture_future, processing_future, writer_future], return_when=concurrent.futures.ALL_COMPLETED ) if not self.canceled: self.status = "处理完成" self.progress = 100 self.print_progress() print(f"\n\n处理完成!输出文件: {self.config['output_path']}") self.logger.info(f"处理完成! 总帧数: {self.frame_counter}, 耗时: {time.time() - self.start_time:.1f}秒") self.logger.info(f"输出文件: {self.config['output_path']}") return True return False except Exception as e: self.status = f"处理过程中发生错误: {str(e)}" error_trace = traceback.format_exc() self.logger.error(f"处理过程中发生错误: {str(e)}") self.logger.error(f"错误详情:\n{error_trace}") print(f"\n\n错误: {str(e)}") return False finally: self.stop_resource_monitor() self.release_resources() if self.config.get('enable_memory_monitor', False): tracemalloc.stop() def release_resources(self): """释放所有资源""" self.logger.info("正在释放资源...") for resource in self.resources: try: if hasattr(resource, 'release'): resource.release() elif hasattr(resource, 'close'): resource.close() except Exception as e: self.logger.warning(f"释放资源时出错: {str(e)}") # 释放CUDA资源 if self.cuda_ctx: try: self.cuda_ctx.destroy() self.logger.info("CUDA上下文已释放") except Exception as e: self.logger.warning(f"释放CUDA上下文时出错: {str(e)}") self.resources = [] self.logger.info("资源已释放") def cancel(self): """取消处理""" self.canceled = True self.status = "正在取消..." self.logger.warning("用户请求取消处理") print("\n正在取消处理...") # 清空队列 while not self.frame_queue.empty(): try: self.frame_queue.get_nowait() self.frame_queue.task_done() except queue.Empty: break while not self.processed_queue.empty(): try: self.processed_queue.get_nowait() self.processed_queue.task_done() except queue.Empty: break self.stop_resource_monitor() self.release_resources() def get_video_info(file_path): """获取视频文件信息""" cap = None try: cap = cv2.VideoCapture(file_path) if cap.isOpened(): width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) fps = cap.get(cv2.CAP_PROP_FPS) frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) duration = frame_count / fps if fps > 0 else 0 return { "width": width, "height": height, "fps": fps, "frame_count": frame_count, "duration": duration } return None except Exception as e: print(f"获取视频信息时出错: {str(e)}") return None finally: if cap and cap.isOpened(): cap.release() def validate_config(config): """验证配置参数""" # 检查文件存在 if not os.path.exists(config['main_video']): print(f"错误: 主视频文件不存在 - {config['main_video']}") return False if not os.path.exists(config['sub_video']): print(f"错误: 副视频文件不存在 - {config['sub_video']}") return False # 检查输出目录 output_dir = os.path.dirname(config['output_path']) if output_dir and not os.path.exists(output_dir): try: os.makedirs(output_dir) print(f"已创建输出目录: {output_dir}") except: print(f"错误: 无法创建输出目录 - {output_dir}") return False # 检查参数有效性 try: # 主视频参数 segment_a = float(config['segment_a']) if segment_a <= 0: print("错误: 分段长度必须大于0!") return False b1 = int(config['b1']) b2 = int(config['b2']) if b1 < 0 or b2 < 0: print("错误: 帧索引不能为负数!") return False if b1 > b2: print("错误: 替换开始帧(b1)必须小于或等于替换结束帧(b2)!") return False # 副视频参数 segment_c = float(config['segment_c']) if segment_c <= 0: print("错误: 分段长度必须大于0!") return False d = int(config['d']) if d < 0: print("错误: 帧索引不能为负数!") return False # 分辨率 width = int(config['output_resolution'][0]) height = int(config['output_resolution'][1]) if width <= 0 or height <= 0: print("错误: 分辨率必须大于0!") return False return True except ValueError: print("错误: 请输入有效的数字参数!") return False def save_config(config, file_path): """保存配置到文件""" try: with open(file_path, 'w') as f: json.dump(config, f, indent=2) print(f"配置已保存到: {file_path}") except Exception as e: print(f"保存配置时出错: {str(e)}") def load_config(file_path): """从文件加载配置""" try: with open(file_path, 'r') as f: config = json.load(f) # 确保配置中包含所有必要字段 required_keys = [ 'main_video', 'sub_video', 'output_path', 'main_segment_type', 'segment_a', 'b1', 'b2', 'sub_segment_type', 'segment_c', 'd', 'sub_option', 'output_resolution' ] for key in required_keys: if key not in config: print(f"警告: 配置文件中缺少 '{key}' 参数") return config except FileNotFoundError: print(f"错误: 配置文件不存在 - {file_path}") except Exception as e: print(f"加载配置时出错: {str(e)}") return None def create_default_config(): """创建默认配置""" return { "main_video": "main_video.mp4", "sub_video": "sub_video.mp4", "output_path": "output/output_video.mp4", "main_segment_type": "秒", # 默认按秒分段 "segment_a": "1", # 默认1秒 "b1": "1", # 默认替换开始帧 "b2": "1", # 默认替换结束帧 "sub_segment_type": "帧", # 默认按帧分段 "segment_c": "1", # 默认1帧 "d": "1", # 默认起始帧 "sub_option": "循环使用", "output_resolution": [1280, 720], "hardware_acceleration": "auto", "gpu_device_index": 0, "reduce_latency": True, "decoding_threads": 4, "use_gpu_processing": True, "cuda_streams": 4, "queue_size": 30, "buffer_size": 3, "target_fps": 30, "use_mjpeg": True, "enable_memory_monitor": False, "mobile_optimized": True # 新增移动端优化标志 } def detect_hardware_acceleration(): """更全面的硬件加速支持检测""" print("\n=== 硬件加速支持检测 ===") print(f"OpenCV版本: {cv2.__version__}") # 检测CUDA支持 if cv2.cuda.getCudaEnabledDeviceCount() > 0: print("CUDA支持: 可用") for i in range(cv2.cuda.getCudaEnabledDeviceCount()): try: device = cv2.cuda.getDevice(i) print(f" 设备 {i}: {device.name()}, 计算能力: {device.majorVersion()}.{device.minorVersion()}") except: print(f" 设备 {i}: 信息获取失败") else: print("CUDA支持: 不可用") # 检测OpenCL支持 print(f"OpenCL支持: {'可用' if cv2.ocl.haveOpenCL() else '不可用'}") # 获取FFMPEG信息 try: result = subprocess.run(['ffmpeg', '-version'], capture_output=True, text=True) ffmpeg_version = result.stdout.split('\n')[0] print(f"FFMPEG版本: {ffmpeg_version}") except: print("FFMPEG版本: 未找到") # 检测可用加速类型 acceleration_types = { 'NVIDIA': cv2.VIDEO_ACCELERATION_NVIDIA, 'Intel': cv2.VIDEO_ACCELERATION_INTEL, 'VAAPI': cv2.VIDEO_ACCELERATION_VAAPI, 'ANY': cv2.VIDEO_ACCELERATION_ANY } print("\n支持的硬件加速类型:") available_accelerations = [] for name, accel_type in acceleration_types.items(): cap = cv2.VideoCapture() try: params = [cv2.CAP_PROP_HW_ACCELERATION, accel_type] test_result = cap.open("", apiPreference=cv2.CAP_FFMPEG, params=params) status = "可用" if test_result else "不可用" print(f"- {name}: {status}") if test_result: available_accelerations.append(name.lower()) except: print(f"- {name}: 检测失败") finally: if cap.isOpened(): cap.release() # 如果没有可用的硬件加速,提供备选方案 if not available_accelerations: print("\n警告: 未检测到任何硬件加速支持!") print("建议:") print("1. 使用软件解码 (设置 hardware_acceleration: 'disable')") print("2. 安装以下备选库:") print(" - NVIDIA GPU 用户: 安装 CUDA Toolkit 和 cuDNN") print(" - Intel GPU 用户: 安装 Intel Media SDK") print(" - AMD/其他 GPU 用户: 安装 VAAPI") print("3. 重新编译OpenCV以支持硬件加速") print("4. 使用支持硬件加速的FFmpeg版本") else: print("\n检测到以下可用的硬件加速类型:") print(", ".join(available_accelerations)) print("在配置文件中设置 'hardware_acceleration' 参数使用") def preview_frame(config, frame_index, is_main=True): """预览指定视频的指定帧""" video_path = config['main_video'] if is_main else config['sub_video'] cap = cv2.VideoCapture(video_path) if not cap.isOpened(): print(f"无法打开视频文件: {video_path}") return total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) if frame_index >= total_frames: print(f"帧索引超出范围 (最大: {total_frames-1})") cap.release() return cap.set(cv2.CAP_PROP_POS_FRAMES, frame_index) ret, frame = cap.read() if ret: # 创建预览窗口 window_name = f"预览: {'主视频' if is_main else '副视频'} - 帧 {frame_index}" cv2.namedWindow(window_name, cv2.WINDOW_NORMAL) # 调整窗口大小 height, width = frame.shape[:2] max_height = 800 if height > max_height: scale = max_height / height frame = cv2.resize(frame, (int(width * scale), max_height)) cv2.imshow(window_name, frame) cv2.waitKey(0) cv2.destroyAllWindows() else: print(f"无法读取帧 {frame_index}") cap.release() def batch_process(config_file, output_dir): """批量处理多个配置""" try: with open(config_file) as f: batch_configs = json.load(f) except Exception as e: print(f"加载批量配置文件失败: {str(e)}") return total_tasks = len(batch_configs) print(f"\n开始批量处理 {total_tasks} 个任务") for i, config in enumerate(batch_configs): print(f"\n处理任务 {i+1}/{total_tasks}") print(f"主视频: {config['main_video']}") print(f"副视频: {config['sub_video']}") # 添加时间戳到输出文件名 timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") base_name = os.path.basename(config['output_path']) config['output_path'] = os.path.join( output_dir, f"{timestamp}_{base_name}" ) # 验证配置 if not validate_config(config): print(f"任务 {i+1} 配置验证失败,跳过") continue # 创建处理器 processor = VideoProcessor(config) success = processor.run() if success: print(f"任务 {i+1} 完成: {config['output_path']}") else: print(f"任务 {i+1} 失败") # 任务间延迟,让系统冷却 if i < total_tasks - 1: print("\n等待5秒,准备下一个任务...") time.sleep(5) def setup_logging(): """配置日志系统""" log_dir = "logs" if not os.path.exists(log_dir): os.makedirs(log_dir) timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") log_file = os.path.join(log_dir, f"video_processor_{timestamp}.log") logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler(log_file), logging.StreamHandler() ] ) logger = logging.getLogger() logger.info(f"日志系统初始化完成, 日志文件: {log_file}") return logger, log_file def install_termux_dependencies(): """安装Termux所需的依赖""" print("正在安装Termux依赖...") commands = [ "pkg update && pkg upgrade -y", "pkg install python libjpeg-turbo libvulkan vulkan-loader-android ffmpeg -y", "pkg install vulkan-tools vulkan-validation-layers -y", "pkg install ocl-icd opencl-headers -y" ] for cmd in commands: print(f"执行: {cmd}") result = subprocess.run(cmd, shell=True) if result.returncode != 0: print(f"命令执行失败: {cmd}") return False print("Termux依赖安装完成") return True def verify_gpu_support(): """验证GPU支持情况""" print("\n验证GPU支持:") # 验证MediaCodec支持 print("\n1. MediaCodec支持:") result = subprocess.run(["ffmpeg", "-hwaccels"], capture_output=True, text=True) if "mediacodec" in result.stdout: print(" ✓ 支持MediaCodec硬件加速") else: print(" ✗ 不支持MediaCodec硬件加速") # 验证Vulkan支持 print("\n2. Vulkan支持:") try: result = subprocess.run(["vulkaninfo"], capture_output=True, text=True) if "deviceName" in result.stdout: print(" ✓ 支持Vulkan API") else: print(" ✗ 不支持Vulkan API") except FileNotFoundError: print(" ✗ vulkaninfo未安装,无法验证Vulkan支持") # 验证OpenCL支持 print("\n3. OpenCL支持:") try: result = subprocess.run(["clinfo"], capture_output=True, text=True) if "Platform Name" in result.stdout: print(" ✓ 支持OpenCL") else: print(" ✗ 不支持OpenCL") except FileNotFoundError: print(" ✗ clinfo未安装,无法验证OpenCL支持") print("\n验证完成") def setup_termux_gpu_acceleration(): """设置Termux GPU加速环境""" print("="*50) print("Termux GPU加速视频处理设置") print("="*50) # 安装基础依赖 if not install_termux_dependencies(): print("依赖安装失败,无法继续设置") return # 验证GPU支持 verify_gpu_support() # 下载并编译CLBlast print("\n编译安装CLBlast...") commands = [ "pkg install git cmake make -y", "git clone https://github.com/CNugteren/CLBlast", "cd CLBlast && mkdir build && cd build", "cmake .. -DCMAKE_INSTALL_PREFIX=$PREFIX", "make install" ] for cmd in commands: print(f"执行: {cmd}") result = subprocess.run(cmd, shell=True) if result.returncode != 0: print(f"命令执行失败: {cmd}") return print("\nGPU加速环境设置完成!") print("现在可以使用以下命令进行硬件加速视频处理:") print("ffmpeg -hwaccel mediacodec -i input.mp4 -c:v h264_mediacodec output.mp4") # 创建示例批处理脚本 with open("gpu_batch_process.sh", "w") as f: f.write("""#!/bin/bash # GPU加速批处理脚本 for f in *.mp4; do echo "处理: $f" ffmpeg -hwaccel mediacodec -i "$f" -c:v h264_mediacodec "gpu_$f" done echo "所有视频处理完成!" """) print("\n已创建批处理脚本: gpu_batch_process.sh") print("使用命令运行: bash gpu_batch_process.sh") def main(): # 设置日志 logger, log_file = setup_logging() # 创建参数解析器 parser = argparse.ArgumentParser(description="专业视频帧替换工具", formatter_class=argparse.RawTextHelpFormatter) parser.add_argument("--config", help="配置文件路径", default="") parser.add_argument("--save-config", help="保存默认配置到文件", action="store_true") parser.add_argument("--background", help="后台运行模式", action="store_true") parser.add_argument("--batch", help="批量处理模式,指定批量配置文件", default="") parser.add_argument("--preview-main", type=int, help="预览主视频指定帧", default=-1) parser.add_argument("--preview-sub", type=int, help="预览副视频指定帧", default=-1) parser.add_argument("--output-dir", help="批量处理输出目录", default="batch_output") parser.add_argument("--enable-gpu", help="启用GPU加速处理", action="store_true") parser.add_argument("--enable-mem-monitor", help="启用内存监控", action="store_true") parser.add_argument("--setup-termux", help="设置Termux GPU加速环境", action="store_true") args = parser.parse_args() # Termux GPU加速设置 if args.setup_termux: setup_termux_gpu_acceleration() return # 保存默认配置 if args.save_config: config_file = args.config if args.config else "video_config.json" default_config = create_default_config() save_config(default_config, config_file) print(f"默认配置已保存到: {config_file}") return # 批量处理模式 if args.batch: if not os.path.exists(args.output_dir): os.makedirs(args.output_dir) batch_process(args.batch, args.output_dir) return # 加载配置 config = None if args.config: config = load_config(args.config) # 如果没有提供配置或加载失败,使用默认配置 if not config: print("使用默认配置") config = create_default_config() # 命令行参数覆盖配置 if args.enable_gpu: config['use_gpu_processing'] = True if args.enable_mem_monitor: config['enable_memory_monitor'] = True # 预览功能 if args.preview_main >= 0: preview_frame(config, args.preview_main, is_main=True) return if args.preview_sub >= 0: preview_frame(config, args.preview_sub, is_main=False) return # 后台模式处理 if args.background: print("后台模式运行中...") logger.info("后台模式启动") # 重定向标准输出到日志 sys.stdout = open(log_file, 'a') sys.stderr = sys.stdout # 显示硬件加速信息 detect_hardware_acceleration() # 显示配置 logger.info("\n当前配置:") logger.info(f"主视频: {config['main_video']}") logger.info(f"副视频: {config['sub_video']}") logger.info(f"输出文件: {config['output_path']}") logger.info(f"主视频分段方式: {config['main_segment_type']}, 长度: {config['segment_a']}") logger.info(f"替换帧范围: b1={config['b1']}, b2={config['b2']}") logger.info(f"副视频分段方式: {config['sub_segment_type']}, 长度: {config['segment_c']}") logger.info(f"副视频起始帧: d={config['d']}") logger.info(f"副视频不足时: {config['sub_option']}") logger.info(f"输出分辨率: {config['output_resolution'][0]}x{config['output_resolution'][1]}") logger.info(f"硬件加速: {config.get('hardware_acceleration', 'auto')}") logger.info(f"解码线程数: {config.get('decoding_threads', 0)}") logger.info(f"使用GPU处理: {config.get('use_gpu_processing', False)}") logger.info(f"CUDA流数量: {config.get('cuda_streams', 0)}") logger.info(f"队列大小: {config.get('queue_size', 30)}") logger.info(f"启用内存监控: {config.get('enable_memory_monitor', False)}") logger.info(f"移动端优化: {config.get('mobile_optimized', True)}") print("\n当前配置:") print(f"主视频: {config['main_video']}") print(f"副视频: {config['sub_video']}") print(f"输出文件: {config['output_path']}") print(f"主视频分段方式: {config['main_segment_type']}, 长度: {config['segment_a']}") print(f"替换帧范围: b1={config['b1']}, b2={config['b2']}") print(f"副视频分段方式: {config['sub_segment_type']}, 长度: {config['segment_c']}") print(f"副视频起始帧: d={config['d']}") print(f"副视频不足时: {config['sub_option']}") print(f"输出分辨率: {config['output_resolution'][0]}x{config['output_resolution'][1]}") print(f"硬件加速: {config.get('hardware_acceleration', 'auto')}") print(f"解码线程数: {config.get('decoding_threads', 0)}") print(f"使用GPU处理: {config.get('use_gpu_processing', False)}") print(f"CUDA流数量: {config.get('cuda_streams', 0)}") print(f"队列大小: {config.get('queue_size', 30)}") print(f"启用内存监控: {config.get('enable_memory_monitor', False)}") print(f"移动端优化: {config.get('mobile_optimized', True)}\n") # 验证配置 if not validate_config(config): logger.error("配置验证失败") return # 显示视频信息 main_info = get_video_info(config['main_video']) if main_info: logger.info("主视频信息:") logger.info(f" 尺寸: {main_info['width']}x{main_info['height']}") logger.info(f" 帧率: {main_info['fps']:.1f} fps") logger.info(f" 总帧数: {main_info['frame_count']}") logger.info(f" 时长: {main_info['duration']:.1f}秒") print("主视频信息:") print(f" 尺寸: {main_info['width']}x{main_info['height']}") print(f" 帧率: {main_info['fps']:.1f} fps") print(f" 总帧数: {main_info['frame_count']}") print(f" 时长: {main_info['duration']:.1f}秒") sub_info = get_video_info(config['sub_video']) if sub_info: logger.info("\n副视频信息:") logger.info(f" 尺寸: {sub_info['width']}x{sub_info['height']}") logger.info(f" 帧率: {sub_info['fps']:.1f} fps") logger.info(f" 总帧数: {sub_info['frame_count']}") logger.info(f" 时长: {sub_info['duration']:.1f}秒") print("\n副视频信息:") print(f" 尺寸: {sub_info['width']}x{sub_info['height']}") print(f" 帧率: {sub_info['fps']:.1f} fps") print(f" 总帧数: {sub_info['frame_count']}") print(f" 时长: {sub_info['duration']:.1f}秒") # 确认开始处理 if not args.background: print("\n按 Enter 开始处理,或输入 'c' 取消...") user_input = input().strip().lower() if user_input == 'c': logger.info("用户取消处理") print("处理已取消") return # 创建并运行处理器 logger.info("开始视频处理") processor = VideoProcessor(config) processor.run() # 保存配置 timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") config_file = f"video_config_{timestamp}.json" save_config(config, config_file) logger.info(f"配置已保存: {config_file}") if __name__ == "__main__": main() 分析此代码所需依赖
06-30
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值