Initial commit: WeChat bot project with AI chat, video export, and image upload scripts
This commit is contained in:
166
video_to_frames.py
Normal file
166
video_to_frames.py
Normal file
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
视频切割脚本 - 将视频逐帧切割为图片
|
||||
支持扫描目录下所有视频文件,多进程并行处理,已切割的自动跳过
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import cv2
|
||||
import argparse
|
||||
from multiprocessing import Pool, cpu_count
|
||||
from pathlib import Path
|
||||
|
||||
# 支持的视频扩展名
|
||||
VIDEO_EXTENSIONS = {'.mp4', '.avi', '.mkv', '.mov', '.flv', '.wmv', '.webm', '.ts', '.m4v'}
|
||||
|
||||
|
||||
def scan_videos(root_path: str) -> list[str]:
|
||||
"""扫描目录下所有视频文件"""
|
||||
videos = []
|
||||
root = Path(root_path)
|
||||
if root.is_file() and root.suffix.lower() in VIDEO_EXTENSIONS:
|
||||
return [str(root)]
|
||||
for p in sorted(root.rglob('*')):
|
||||
if p.is_file() and p.suffix.lower() in VIDEO_EXTENSIONS:
|
||||
videos.append(str(p))
|
||||
return videos
|
||||
|
||||
|
||||
def extract_frames(video_path: str, output_dir: str = None) -> dict:
|
||||
"""
|
||||
将单个视频按每秒1帧切割为图片
|
||||
|
||||
Args:
|
||||
video_path: 视频文件路径
|
||||
output_dir: 输出目录(默认在视频同目录下创建同名文件夹)
|
||||
|
||||
Returns:
|
||||
处理结果字典
|
||||
"""
|
||||
video_path = os.path.abspath(video_path)
|
||||
video_name = Path(video_path).stem
|
||||
|
||||
if output_dir is None:
|
||||
output_dir = os.path.join(os.path.dirname(video_path), video_name + '_frames')
|
||||
|
||||
# 已经切割过则跳过
|
||||
if os.path.isdir(output_dir) and any(Path(output_dir).glob('*.jpg')):
|
||||
existing = len(list(Path(output_dir).glob('*.jpg')))
|
||||
return {
|
||||
'video': video_path,
|
||||
'status': 'skipped',
|
||||
'reason': f'已存在,共 {existing} 张图片',
|
||||
'output': output_dir,
|
||||
}
|
||||
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
cap = cv2.VideoCapture(video_path)
|
||||
if not cap.isOpened():
|
||||
return {
|
||||
'video': video_path,
|
||||
'status': 'error',
|
||||
'reason': '无法打开视频文件',
|
||||
'output': output_dir,
|
||||
}
|
||||
|
||||
fps = cap.get(cv2.CAP_PROP_FPS) or 25
|
||||
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
||||
duration = total_frames / fps if fps else 0
|
||||
|
||||
# 每秒取一帧:根据fps算出每隔多少帧取一帧
|
||||
frame_interval = int(fps)
|
||||
frame_idx = 0
|
||||
saved = 0
|
||||
while True:
|
||||
ret, frame = cap.read()
|
||||
if not ret:
|
||||
break
|
||||
if frame_idx % frame_interval == 0:
|
||||
# 用视频名+时间戳命名:第01集_01m23s_456.jpg
|
||||
sec = frame_idx / fps
|
||||
mins = int(sec // 60)
|
||||
secs = int(sec % 60)
|
||||
ms = int((sec % 1) * 1000)
|
||||
filename = os.path.join(output_dir, f'{video_name}_{mins:02d}m{secs:02d}s_{ms:03d}.jpg')
|
||||
cv2.imwrite(filename, frame)
|
||||
saved += 1
|
||||
frame_idx += 1
|
||||
|
||||
cap.release()
|
||||
|
||||
return {
|
||||
'video': video_path,
|
||||
'status': 'done',
|
||||
'frames_total': total_frames,
|
||||
'frames_saved': saved,
|
||||
'duration': f'{duration:.1f}s',
|
||||
'fps': fps,
|
||||
'output': output_dir,
|
||||
}
|
||||
|
||||
|
||||
def _worker(args):
|
||||
"""多进程 worker 包装"""
|
||||
video_path, output_dir = args
|
||||
try:
|
||||
return extract_frames(video_path, output_dir)
|
||||
except Exception as e:
|
||||
return {
|
||||
'video': video_path,
|
||||
'status': 'error',
|
||||
'reason': str(e),
|
||||
}
|
||||
|
||||
|
||||
def main(video_path, output=None, interval=1, jobs=None):
|
||||
videos = scan_videos(video_path)
|
||||
if not videos:
|
||||
print(f'未在 {video_path} 中找到视频文件')
|
||||
return
|
||||
|
||||
jobs = jobs or cpu_count()
|
||||
print(f'找到 {len(videos)} 个视频,使用 {jobs} 个进程并行处理')
|
||||
|
||||
tasks = []
|
||||
for v in videos:
|
||||
if output:
|
||||
video_name = Path(v).stem
|
||||
out = os.path.join(output, video_name + '_frames')
|
||||
else:
|
||||
out = None
|
||||
tasks.append((v, out))
|
||||
|
||||
done = 0
|
||||
skipped = 0
|
||||
errors = 0
|
||||
|
||||
with Pool(jobs) as pool:
|
||||
for result in pool.imap_unordered(_worker, tasks):
|
||||
status = result['status']
|
||||
video = result['video']
|
||||
if status == 'done':
|
||||
done += 1
|
||||
print(f'[完成] {video} -> {result["frames_saved"]}帧 '
|
||||
f'(总{result["frames_total"]}, {result["duration"]})')
|
||||
elif status == 'skipped':
|
||||
skipped += 1
|
||||
print(f'[跳过] {video} ({result["reason"]})')
|
||||
else:
|
||||
errors += 1
|
||||
print(f'[错误] {video} ({result["reason"]})')
|
||||
|
||||
print(f'\n处理完毕: 完成 {done}, 跳过 {skipped}, 错误 {errors}, 共 {len(videos)}')
|
||||
|
||||
|
||||
# ========== 运行设置(改这里)==========
|
||||
VIDEO_DIR = "./shipin" # 视频文件或目录路径
|
||||
OUTPUT_DIR = None # 输出目录,None 则在视频同目录
|
||||
INTERVAL = 1 # 每隔 N 帧取一帧
|
||||
JOBS = None # 并行进程数,None 则用 CPU 核心数
|
||||
# ======================================
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(VIDEO_DIR, OUTPUT_DIR, INTERVAL, JOBS)
|
||||
Reference in New Issue
Block a user