python mp4.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
MP4视频帧转高清图片工具
使用FFmpeg提取视频帧并转换为高质量PNG/JPEG格式
"""import os
import subprocess
import sys
from pathlib import Path
import argparseclass MP4ToImageConverter:def __init__(self):self.ffmpeg_path = self._find_ffmpeg()def _find_ffmpeg(self):"""查找FFmpeg可执行文件路径"""try:# 尝试直接调用ffmpegresult = subprocess.run(['ffmpeg', '-version'], capture_output=True, text=True, timeout=5)if result.returncode == 0:return 'ffmpeg'except (subprocess.TimeoutExpired, FileNotFoundError):pass# 在常见路径中查找common_paths = ['ffmpeg','/usr/bin/ffmpeg','/usr/local/bin/ffmpeg','/opt/homebrew/bin/ffmpeg', # macOS Homebrew'C:/ffmpeg/bin/ffmpeg.exe', # Windows'C:/Program Files/ffmpeg/bin/ffmpeg.exe']for path in common_paths:if os.path.exists(path) or self._test_ffmpeg_path(path):return pathraise FileNotFoundError("未找到FFmpeg,请确保已安装FFmpeg并添加到PATH环境变量")def _test_ffmpeg_path(self, path):"""测试FFmpeg路径是否有效"""try:result = subprocess.run([path, '-version'], capture_output=True, text=True, timeout=5)return result.returncode == 0except:return Falsedef get_video_info(self, input_file):"""获取视频信息"""cmd = [self.ffmpeg_path, '-i', input_file,'-hide_banner', '-f', 'null', '-']try:result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)# FFmpeg将信息输出到stderrinfo = result.stderrreturn infoexcept subprocess.TimeoutExpired:raise Exception("获取视频信息超时")except Exception as e:raise Exception(f"获取视频信息失败: {e}")def extract_frames_to_images(self, input_file, output_dir, **options):"""提取视频帧并转换为图片格式参数:- input_file: 输入MP4文件路径- output_dir: 输出目录- options: 转换选项"""# 创建输出目录output_path = Path(output_dir)output_path.mkdir(parents=True, exist_ok=True)# 设置默认参数format_type = options.get('format', 'png').lower() # 图片格式: png, jpg, jpegquality = options.get('quality', 95) # JPEG质量 (1-100)fps = options.get('fps', None) # 提取帧率scale = options.get('scale', None) # 缩放比例start_time = options.get('start_time', None) # 开始时间duration = options.get('duration', None) # 持续时间# 标准化格式if format_type in ['jpg', 'jpeg']:format_type = 'jpg'file_ext = 'jpg'else:format_type = 'png'file_ext = 'png'# 构建FFmpeg命令cmd = [self.ffmpeg_path, '-i', input_file]# 添加时间相关参数if start_time:cmd.extend(['-ss', str(start_time)])if duration:cmd.extend(['-t', str(duration)])# 视频滤镜参数video_filters = []# 帧率控制if fps:video_filters.append(f'fps={fps}')# 分辨率缩放if scale:if isinstance(scale, (int, float)):# 按比例缩放video_filters.append(f'scale=iw*{scale}:ih*{scale}')elif isinstance(scale, str) and 'x' in scale:# 指定分辨率,如 "1920x1080"video_filters.append(f'scale={scale}')# 应用视频滤镜if video_filters:cmd.extend(['-vf', ','.join(video_filters)])# 图片编码参数if format_type == 'png':# PNG - 无损压缩cmd.extend(['-c:v', 'png','-pix_fmt', 'rgb24' # RGB格式,确保兼容性])else:# JPEG - 有损压缩cmd.extend(['-c:v', 'mjpeg','-q:v', str(int((100-quality)*0.31+1)), # FFmpeg的质量范围是1-31,数值越小质量越高'-pix_fmt', 'yuvj420p' # JPEG标准格式])# 输出文件模式output_pattern = str(output_path / f"frame_%06d.{file_ext}")cmd.append(output_pattern)# 添加通用参数cmd.extend(['-hide_banner', '-y']) # 隐藏banner,覆盖输出文件print(f"开始转换: {input_file}")print(f"输出目录: {output_dir}")print(f"图片格式: {format_type.upper()}")if format_type == 'jpg':print(f"JPEG质量: {quality}%")print(f"执行命令: {' '.join(cmd)}")try:# 执行转换process = subprocess.run(cmd, capture_output=True, text=True, timeout=3600 # 1小时超时)if process.returncode == 0:# 统计生成的文件image_files = list(output_path.glob(f"*.{file_ext}"))print(f"✅ 转换完成!共生成 {len(image_files)} 张{format_type.upper()}图片")# 显示文件大小信息total_size = sum(f.stat().st_size for f in image_files)print(f"总文件大小: {total_size / 1024 / 1024:.2f} MB")if image_files:avg_size = total_size / len(image_files) / 1024print(f"平均单张大小: {avg_size:.1f} KB")return Trueelse:print(f"❌ 转换失败:")print(f"错误信息: {process.stderr}")# 如果是编码器问题,尝试基础转换if "Unknown encoder" in process.stderr:print("🔄 尝试使用基础图片编码器...")return self._basic_conversion(input_file, output_dir, format_type, options)return Falseexcept subprocess.TimeoutExpired:print("❌ 转换超时")return Falseexcept Exception as e:print(f"❌ 转换过程中出现错误: {e}")return Falsedef _basic_conversion(self, input_file, output_dir, format_type, options):"""基础转换方法,使用最兼容的参数"""output_path = Path(output_dir)output_path.mkdir(parents=True, exist_ok=True)fps = options.get('fps', None)scale = options.get('scale', None)start_time = options.get('start_time', None)duration = options.get('duration', None)if format_type == 'png':file_ext = 'png'else:file_ext = 'jpg'# 构建最基础的FFmpeg命令cmd = [self.ffmpeg_path, '-i', input_file]if start_time:cmd.extend(['-ss', str(start_time)])if duration:cmd.extend(['-t', str(duration)])# 视频滤镜video_filters = []if fps:video_filters.append(f'fps={fps}')if scale:if isinstance(scale, (int, float)):video_filters.append(f'scale=iw*{scale}:ih*{scale}')elif isinstance(scale, str) and 'x' in scale:video_filters.append(f'scale={scale}')if video_filters:cmd.extend(['-vf', ','.join(video_filters)])# 使用最基本的参数if format_type == 'png':cmd.extend(['-f', 'image2']) # 强制图像格式else:quality = options.get('quality', 95)cmd.extend(['-q:v', str(int((100-quality)*0.31+1))])output_pattern = str(output_path / f"frame_%06d.{file_ext}")cmd.extend([output_pattern, '-hide_banner', '-y'])print(f"基础转换命令: {' '.join(cmd)}")try:process = subprocess.run(cmd, capture_output=True, text=True, timeout=3600)if process.returncode == 0:image_files = list(output_path.glob(f"*.{file_ext}"))print(f"✅ 基础转换成功!共生成 {len(image_files)} 张{format_type.upper()}图片")total_size = sum(f.stat().st_size for f in image_files)print(f"总文件大小: {total_size / 1024 / 1024:.2f} MB")return Trueelse:print(f"❌ 基础转换也失败了:")print(f"错误信息: {process.stderr}")return Falseexcept Exception as e:print(f"❌ 基础转换出现错误: {e}")return Falsedef batch_convert(self, input_dir, output_base_dir, **options):"""批量转换目录中的所有MP4文件"""input_path = Path(input_dir)mp4_files = list(input_path.glob("*.mp4")) + list(input_path.glob("*.MP4"))if not mp4_files:print(f"在目录 {input_dir} 中未找到MP4文件")returnprint(f"找到 {len(mp4_files)} 个MP4文件")success_count = 0for mp4_file in mp4_files:print(f"\n处理文件: {mp4_file.name}")# 为每个视频创建单独的输出目录video_output_dir = Path(output_base_dir) / mp4_file.stemif self.extract_frames_to_images(str(mp4_file), str(video_output_dir), **options):success_count += 1print(f"\n批量转换完成: {success_count}/{len(mp4_files)} 个文件转换成功")def main():parser = argparse.ArgumentParser(description='MP4视频帧转高清图片工具')parser.add_argument('input', help='输入MP4文件或目录路径')parser.add_argument('-o', '--output', help='输出目录路径', default='./image_frames')parser.add_argument('-f', '--format', choices=['png', 'jpg', 'jpeg'], default='png',help='输出图片格式 (默认: png)')parser.add_argument('-q', '--quality', type=int, default=95, help='JPEG质量 (1-100,默认95,仅对JPEG有效)')parser.add_argument('--fps', type=float, help='提取帧率 (如: 1表示每秒1帧)')parser.add_argument('--scale', help='缩放比例或分辨率 (如: 0.5 或 1920x1080)')parser.add_argument('--start', help='开始时间 (如: 00:01:30)')parser.add_argument('--duration', help='持续时间 (如: 00:05:00)')parser.add_argument('--batch', action='store_true', help='批量处理目录中的所有MP4文件')args = parser.parse_args()# 验证输入路径if not os.path.exists(args.input):print(f"错误: 输入路径不存在: {args.input}")sys.exit(1)# 创建转换器try:converter = MP4ToImageConverter()print("✅ FFmpeg已就绪")except FileNotFoundError as e:print(f"错误: {e}")sys.exit(1)# 准备转换选项options = {'format': args.format,'quality': args.quality}if args.fps:options['fps'] = args.fpsif args.scale:try:# 尝试解析为数字options['scale'] = float(args.scale)except ValueError:# 作为字符串处理 (分辨率格式)options['scale'] = args.scaleif args.start:options['start_time'] = args.startif args.duration:options['duration'] = args.duration# 执行转换if args.batch or os.path.isdir(args.input):converter.batch_convert(args.input, args.output, **options)else:converter.extract_frames_to_images(args.input, args.output, **options)if __name__ == "__main__":# 如果没有命令行参数,提供交互式界面if len(sys.argv) == 1:print("=== MP4转高清图片工具 ===\n")try:converter = MP4ToImageConverter()print("✅ FFmpeg已就绪")except FileNotFoundError as e:print(f"错误: {e}")sys.exit(1)# 交互式输入input_file = input("请输入MP4文件路径: ").strip().strip('"')if not os.path.exists(input_file):print("文件不存在!")sys.exit(1)output_dir = input("请输入输出目录 (默认: ./image_frames): ").strip() or "./image_frames"print("\n=== 图片格式选择 ===")print("1. PNG - 无损格式,质量最高,文件较大")print("2. JPEG - 有损格式,质量可调,文件较小")format_choice = input("请选择格式 1/2 (默认: 1): ").strip() or "1"if format_choice == "2":img_format = "jpg"quality_input = input("JPEG质量 1-100 (默认: 95): ").strip()quality = int(quality_input) if quality_input else 95else:img_format = "png"quality = 95 # PNG不需要质量参数,但保留用于兼容print("\n=== 高级选项 (直接回车使用默认值) ===")fps_input = input("提取帧率,如1表示每秒1帧 (默认: 原始帧率): ").strip()fps = float(fps_input) if fps_input else Nonescale_input = input("缩放比例或分辨率,如0.5或1920x1080 (默认: 原始大小): ").strip()scale = scale_input if scale_input else Nonetry:if scale and scale.replace('.', '').isdigit():scale = float(scale)except:pass# 执行转换options = {'format': img_format,'quality': quality}if fps:options['fps'] = fpsif scale:options['scale'] = scaleconverter.extract_frames_to_images(input_file, output_dir, **options)else:main()