吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 287|回复: 2
收起左侧

[其他求助] 求助 ffmpeg能截帧yuv420p像素格式吗?

[复制链接]
8veniE3Lie 发表于 2026-7-6 20:00
100吾爱币
求助 ffmpeg能截帧yuv420p像素格式吗?

用ffmpeg截帧出来怎么都无法截yuv420p原视频色彩是yuv420p但截出来的yuvj420p这个yuv420p手机是无法截吗?照片格式是JPEG我嫌png格式太大了

求助 红果短剧视频怎么下载

手机红果短剧和其他软件的短剧怎么下载1080高码或者普通的1080视频有什么工具或者脚本吗挺需要的

最佳答案

查看完整内容

可以的,FFmpeg 完全支持截取 YUV420P 像素格式的帧。 FFmpeg 的 -pix_fmt yuv420p 参数可以强制将任何视频帧转换为 YUV420P 格式并输出。 核心命令模板 bash ffmpeg -i input.mp4 -pix_fmt yuv420p -f rawvideo output.yuv 参数说明: -i input.mp4:指定输入视频文件。 -pix_fmt yuv420p:强制将输出像素格式设为 YUV420P。 -f rawvideo:指定输出为原始的 YUV 数据流。 output.yuv:输出文件名。 几种 ...

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

shzjz123 发表于 2026-7-6 20:00
可以的,FFmpeg 完全支持截取 YUV420P 像素格式的帧。

FFmpeg 的 -pix_fmt yuv420p 参数可以强制将任何视频帧转换为 YUV420P 格式并输出。

核心命令模板
bash
ffmpeg -i input.mp4 -pix_fmt yuv420p -f rawvideo output.yuv
参数说明:

-i input.mp4:指定输入视频文件。

-pix_fmt yuv420p:强制将输出像素格式设为 YUV420P。

-f rawvideo:指定输出为原始的 YUV 数据流。

output.yuv:输出文件名。

几种常见用法示例
1. 只提取特定的一帧

使用 -vf 滤镜选择帧,或用 -vframes 1 限制只输出一帧。

提取第 100 帧:

bash
ffmpeg -i input.mp4 -vf "select=eq(n\,100)" -pix_fmt yuv420p -vsync vfr frame_100.yuv

提取第 10 秒的那一帧:

bash
ffmpeg -i input.mp4 -ss 00:00:10 -vframes 1 -pix_fmt yuv420p -f rawvideo frame_10s.yuv

2. 按一定频率提取所有帧

bash
ffmpeg -i input.mp4 -vf "fps=1" -pix_fmt yuv420p frame_%04d.yuv
这个命令会每秒提取一帧,并保存为 frame_0001.yuv、frame_0002.yuv 等文件。

3. 限制提取时长或画面大小

只处理前 3 秒:添加 -t 3 参数。

缩放到指定尺寸:添加 -s 320x240 参数。

bash
ffmpeg -i input.mp4 -t 3 -s 320x240 -pix_fmt yuv420p -f rawvideo output.yuv
⚠️ 重要提示
文件巨大:YUV 格式是未压缩的原始视频数据,体积非常庞大。例如,一个原本 13MB 的 1 分钟视频,导出为 YUV420P 后可能达到 837MB。

播放需参数:播放 .yuv 文件时,必须明确告知播放器视频的分辨率和像素格式。

bash
ffplay -pixel_format yuv420p -video_size 1920x1080 -framerate 30 -i output.yuv

所以,FFmpeg 不仅能截取 YUV420P 帧,而且这是它非常基础且强大的功能之一。
 楼主| 8veniE3Lie 发表于 2026-7-7 13:10
shzjz123 发表于 2026-7-6 20:00
可以的,FFmpeg 完全支持截取 YUV420P 像素格式的帧。

FFmpeg 的 -pix_fmt yuv420p 参数可以强制将任何 ...

想问一下如果还用JPEG格式能行吗,豆包和deekseep一直改不好
能帮忙
[Python] 纯文本查看 复制代码
import os
import subprocess
import re
import json
import secrets
import tempfile
import time
import threading
from typing import Optional, Tuple, Dict, List

# ====================== 配置项 ======================
input_dir = "/storage/emulated/0/视频分割/"
output_dir = os.path.join(input_dir, "分割完成")
frame_dir = "/storage/emulated/0/JPEG截帧/"
video_extensions = (".mp4", ".mkv", ".mov", ".avi", ".flv", ".webm")
image_extensions = (".jpg", ".jpeg", ".png", ".bmp", ".webp")
last_custom_fps = None
# ★ 存储格式:(clip_path, start_sec, dur, out_base)
clip_history_list = []
_color_params_cache = None
_cached_target_path = None
_target_time_cache = None

# 运行期间记忆
_last_tonemap = True
_nf_preset = False
last_scheme_code = None
_first_frame_extract_flag = True   # 控制滤镜信息只打印一次
_global_remembered_fps = None      # 全终端帧率记忆

# ====================== 原子唯一签名注入器 ======================
class AtomicUniqueInjector:
    def __init__(self):
        self._counter = 0
        self._lock = threading.Lock()
        self._instance_id = secrets.token_bytes(16)
        
    def inject(self, file_path):
        with self._lock:
            self._counter += 1
            nano_time = time.time_ns()
            counter_bytes = self._counter.to_bytes(8, 'big')
            random_bytes = secrets.token_bytes(32)
            unique_signature = (
                self._instance_id +
                nano_time.to_bytes(8, 'big') +
                counter_bytes +
                random_bytes
            )
            try:
                with open(file_path, "ab", buffering=0) as f:
                    f.write(unique_signature)
                return True
            except Exception:
                return False

_unique_injector = AtomicUniqueInjector()

def check_exit(s):
    txt = s.strip().lower()
    if txt in ["退出", "tc"]:
        print("\n👋 程序已强制退出,所有设置未保存")
        os._exit(0)

def fast_modify_md5(file_path):
    try:
        _unique_injector.inject(file_path)
    except Exception:
        pass

# ====================== LLC 时间格式化函数 ======================
def sec_to_time_llc(total_sec):
    ms_total = int(total_sec * 1000)
    hour = ms_total // 3600000
    remain = ms_total % 3600000
    minute = remain // 60000
    remain = remain % 60000
    second = remain // 1000
    millisec = remain % 1000
    return f"{hour:02d}.{minute:02d}.{second:02d}.{millisec:03d}"

# ==============================================
# 视频参数探测
# ==============================================
def probe_color_params(video_path: str) -> Dict:
    cmd = [
        "ffprobe", "-hide_banner",
        "-select_streams", "v:0",
        "-show_entries",
        "stream=color_primaries,color_transfer,color_space,pix_fmt,width,height,r_frame_rate,bit_depth,color_range",
        "-of", "json",
        video_path
    ]
    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
        data = json.loads(result.stdout)
        streams = data.get("streams", [])
        if not streams:
            return {"error": "no stream"}
        stream = streams[0]
        color_range = stream.get("color_range", "unknown").lower()
        return {
            "color_primaries": stream.get("color_primaries", "unknown"),
            "color_transfer": stream.get("color_transfer", "unknown"),
            "color_space": stream.get("color_space", "unknown"),
            "pix_fmt": stream.get("pix_fmt", "unknown"),
            "width": stream.get("width", 0),
            "height": stream.get("height", 0),
            "r_frame_rate": stream.get("r_frame_rate", "24000/1001"),
            "bit_depth": stream.get("bit_depth", 8),
            "color_range": color_range,
        }
    except Exception as e:
        return {"error": str(e)}

def is_hdr_video(params: Dict) -> bool:
    cp = params.get("color_primaries", "").lower()
    ct = params.get("color_transfer", "").lower()
    cs = params.get("color_space", "").lower()
    pf = params.get("pix_fmt", "").lower()
    hdr_keywords = ["bt2020", "smpte2084", "arib-std-b67", "pq", "hlg"]
    if any(k in cp or k in ct or k in cs for k in hdr_keywords):
        return True
    if ("10le" in pf or "12le" in pf) and ("bt2020" in cs or "smpte" in ct):
        return True
    return False

def show_color_info(video_path):
    params = probe_color_params(video_path)
    if "error" in params:
        print(f"⚠️ 无法获取色彩参数: {params['error']}")
        return None
    print("\n📊 原始视频色彩参数:")
    print(f"   色彩空间        : {params.get('color_space', '?')}")
    print(f"   色彩原色        : {params.get('color_primaries', '?')}")
    print(f"   传输特性        : {params.get('color_transfer', '?')}")
    print(f"   像素格式        : {params.get('pix_fmt', '?')}")
    print(f"   位深            : {params.get('bit_depth', '?')} bit")
    print(f"   分辨率          : {params.get('width')}x{params.get('height')}")
    print(f"   色彩范围        : {params.get('color_range', '?')}")
    print(f"   HDR             : {'是' if is_hdr_video(params) else '否'}")
    return params

# ==============================================
# 色彩匹配
# ==============================================
def find_target_color_file(exclude_file=None):
    if not os.path.exists(input_dir):
        return None
    all_extensions = video_extensions + image_extensions
    for f in sorted(os.listdir(input_dir)):
        fp = os.path.join(input_dir, f)
        if "分割完成" in fp or "分割" in f:
            continue
        if os.path.isfile(fp) and f.lower().endswith(all_extensions):
            if exclude_file and f == exclude_file:
                continue
            return fp
    return None

def is_image_file(file_path):
    return file_path.lower().endswith(image_extensions)

def input_target_time_range(target_path):
    global _target_time_cache
    if is_image_file(target_path):
        print("📷 目标为图片文件,直接采样")
        return 0, 1
    duration = get_video_duration(target_path)
    while True:
        u = input("\n🎨 色彩采样时间范围(格式:MM:SS-MM:SS 或 回车=整个视频):").strip()
        check_exit(u)
        if not u:
            _target_time_cache = (0, duration)
            return 0, duration
        parts = u.split('-')
        if len(parts) != 2:
            print("⚠️ 格式:开始时间-结束时间(如:00:30-01:30)")
            continue
        start_str, end_str = parts[0].strip(), parts[1].strip()
        if not validate_time(start_str) or not validate_time(end_str):
            print("⚠️ 时间格式错误,使用 MM:SS 或 HH:MM:SS")
            continue
        start_sec = time_to_sec(start_str)
        end_sec = time_to_sec(end_str)
        if start_sec >= end_sec:
            print("❌ 结束时间必须大于开始时间")
            continue
        if end_sec > duration:
            end_sec = duration
        _target_time_cache = (start_sec, end_sec)
        return start_sec, end_sec

def get_color_params(file_path, target_path=None, start_sec=0, duration=None):
    global _color_params_cache, _cached_target_path, _target_time_cache
    if target_path is None:
        target_path = find_target_color_file()
    if not target_path:
        return (128, 128, 128)
    if file_path == target_path and _color_params_cache is not None and _cached_target_path == target_path:
        return _color_params_cache
    is_img = is_image_file(target_path)
    tmp_raw = os.path.join(tempfile.gettempdir(), f"color_{secrets.token_hex(4)}.raw")
    if is_img:
        cmd_sample = [
            "ffmpeg", "-hide_banner", "-loglevel", "error",
            "-i", target_path,
            "-vf", "scale=1:1:flags=area,format=rgb24",
            "-vframes", "1",
            "-f", "rawvideo",
            "-y", tmp_raw
        ]
    else:
        if duration is None:
            duration = min(3, get_video_duration(target_path))
        sample_count = min(10, int(duration * 2))
        cmd_sample = [
            "ffmpeg", "-hide_banner", "-loglevel", "error",
            "-ss", str(start_sec),
            "-i", target_path,
            "-t", str(duration),
            "-vf", "scale=1:1:flags=area,format=rgb24",
            "-vsync", "0",
            "-vframes", str(sample_count),
            "-f", "rawvideo",
            "-y", tmp_raw
        ]
    subprocess.run(cmd_sample, capture_output=True)
    r_sum = g_sum = b_sum = 0
    count = 0
    if os.path.exists(tmp_raw):
        with open(tmp_raw, "rb") as f:
            data = f.read()
        os.remove(tmp_raw)
        for i in range(0, len(data), 3):
            if i + 2 < len(data):
                r_sum += data[i]
                g_sum += data[i+1]
                b_sum += data[i+2]
                count += 1
    if count == 0:
        return (128, 128, 128)
    r_avg = r_sum // count
    g_avg = g_sum // count
    b_avg = b_sum // count
    if file_path == target_path:
        _color_params_cache = (r_avg, g_avg, b_avg)
        _cached_target_path = target_path
    return (r_avg, g_avg, b_avg)

def build_color_match_filter(source_video, target_path=None):
    if target_path is None:
        target_path = find_target_color_file()
    if not target_path:
        return None
    src_r, src_g, src_b = get_color_params(source_video)
    tgt_r, tgt_g, tgt_b = get_color_params(target_path, target_path=target_path)
    diff_r = tgt_r - src_r
    diff_g = tgt_g - src_g
    diff_b = tgt_b - src_b
    if abs(diff_r) < 5 and abs(diff_g) < 5 and abs(diff_b) < 5:
        return None
    return (diff_b, diff_g, diff_r)

def setup_color_target(exclude_file=None):
    global _color_params_cache, _cached_target_path, _target_time_cache
    all_extensions = video_extensions + image_extensions
    all_files = []
    for f in sorted(os.listdir(input_dir)):
        fp = os.path.join(input_dir, f)
        if "分割完成" in fp or "分割" in f:
            continue
        if os.path.isfile(fp) and f.lower().endswith(all_extensions):
            if exclude_file and f == exclude_file:
                continue
            all_files.append(f)
    if len(all_files) <= 1:
        _color_params_cache = None
        _cached_target_path = None
        _target_time_cache = None
        return None
    target_path = find_target_color_file(exclude_file)
    if not target_path:
        return None
    _color_params_cache = None
    _cached_target_path = None
    _target_time_cache = None
    start_sec, end_sec = input_target_time_range(target_path)
    get_color_params(target_path, target_path=target_path, start_sec=start_sec, duration=end_sec-start_sec)
    return target_path

# ==============================================
# 时间处理
# ==============================================
def time_to_sec(time_str):
    t = time_str.strip()
    if t.isdigit():
        if len(t) == 1 or len(t) == 2:
            return -1
        if len(t) == 3 or len(t) == 4:
            t = t.zfill(4)
            t = f"{t[:2]}:{t[2:]}"
        elif len(t) == 5 or len(t) == 6:
            t = t.zfill(6)
            t = f"{t[:2]}:{t[2:4]}:{t[4:]}"
        else:
            return -1
    try:
        parts = list(map(float, t.split(':')))
        if len(parts) == 2:
            m, s = parts
            h = 0
        elif len(parts) == 3:
            h, m, s = parts
        else:
            return -1
        return h * 3600 + m * 60 + s
    except:
        return -1

def validate_time(time_str):
    t = time_str.strip()
    pat = r'^\d{1,2}:\d{1,2}(\.\d+)?$|^\d{1,2}:\d{1,2}:\d{1,2}(\.\d+)?$'
    if re.match(pat, t):
        return True
    if t.isdigit() and 3 <= len(t) <= 6:
        return True
    return False

# ==============================================
# 基础功能函数
# ==============================================
def get_fps_fraction(video_path):
    cmd = [
        "ffprobe", "-v", "error",
        "-select_streams", "v:0",
        "-show_entries", "stream=r_frame_rate",
        "-of", "noprint_wrappers=1:nokey=1",
        video_path
    ]
    res = subprocess.run(cmd, capture_output=True, text=True)
    raw = res.stdout.strip()
    if "/" in raw:
        a_str, b_str = raw.split("/")
        a = int(a_str)
        b = int(b_str)
    else:
        a = 24000
        b = 1001
    return a, b

def get_video_duration(video_path):
    cmd = [
        "ffprobe", "-v", "error",
        "-show_entries", "format=duration",
        "-of", "csv=p=0",
        video_path
    ]
    res = subprocess.run(cmd, capture_output=True, text=True)
    try:
        return float(res.stdout.strip())
    except:
        return 0

def get_video_fps_display(video_path):
    a, b = get_fps_fraction(video_path)
    return round(a / b)

def check_ffmpeg():
    try:
        subprocess.run(["ffmpeg", "-version"], capture_output=True, text=True, check=True)
        return True
    except:
        return False

# ==============================================
# 文件扫描与选择
# ==============================================
def scan_original_videos():
    res = []
    for f in os.listdir(input_dir):
        fp = os.path.join(input_dir, f)
        if (os.path.isfile(fp) and "分割完成" not in fp
            and "分割" not in f and f.lower().endswith(video_extensions)):
            res.append(f)
    return sorted(res)

def select_video(target_path=None):
    while True:
        lst = scan_original_videos()
        if not lst:
            u = input("\n无视频,放入视频后回车 / 输入tc退出 / qbjz扫描全部视频:")
            check_exit(u)
            if u.strip().lower() == "qbjz":
                qbjz_scan_input_dir_and_process(target_path)
                continue
            continue
        if len(lst) == 1:
            v = lst[0]
            p = os.path.join(input_dir, v)
            name, ext = os.path.splitext(v)
            print(f"\n已自动选中:{v}")
            return v, p, name, ext
        print("\n===== 视频列表 =====")
        for i, v in enumerate(lst):
            print(f"{i+1}. {v}")
        while True:
            c = input("\n输入序号选择 | tc退出 | qbjz扫描全部视频:")
            check_exit(c)
            if c.strip().lower() == "qbjz":
                qbjz_scan_input_dir_and_process(target_path)
                continue
            if not c.isdigit():
                print("请输入数字")
                continue
            idx = int(c) - 1
            if 0 <= idx < len(lst):
                v = lst[idx]
                p = os.path.join(input_dir, v)
                name, ext = os.path.splitext(v)
                print(f"\n已选中:{v}")
                return v, p, name, ext

def parse_selection(user_input, max_num):
    user_input = user_input.strip()
    if not user_input:
        return None
    if '-' in user_input:
        parts = user_input.split('-')
        if len(parts) != 2:
            return None
        try:
            start = int(parts[0].strip())
            end = int(parts[1].strip())
        except ValueError:
            return None
        if start < 1 or end < 1 or start > end or end > max_num:
            return None
        return list(range(start-1, end))
    if ',' in user_input:
        parts = user_input.split(',')
        indices = []
        for p in parts:
            p = p.strip()
            if not p.isdigit():
                return None
            num = int(p)
            if num < 1 or num > max_num:
                return None
            indices.append(num-1)
        return sorted(list(set(indices)))
    if user_input.isdigit():
        num = int(user_input)
        if 1 <= num <= max_num:
            return [num-1]
    return None

def select_video_from_output():
    if not os.path.exists(output_dir):
        print(f"&#9888;&#65039; 输出目录不存在: {output_dir}")
        return []

    videos = []
    for f in os.listdir(output_dir):
        fp = os.path.join(output_dir, f)
        if os.path.isfile(fp) and f.lower().endswith(video_extensions):
            videos.append(f)

    if not videos:
        print("&#9888;&#65039; 分割完成目录中没有视频文件")
        return []

    videos = sorted(videos)
    if len(videos) == 1:
        v = videos[0]
        p = os.path.join(output_dir, v)
        name, ext = os.path.splitext(v)
        print(f"\n自动选中切割后视频:{v}")
        return [(v, p, name, ext)]

    print("\n===== 已切割视频列表 =====")
    for i, v in enumerate(videos):
        print(f"{i+1}. {v}")
    print("支持输入:单个序号(1)  范围(1-3)  列表(1,3,5)")

    while True:
        c = input("\n输入选择 | tc退出:")
        check_exit(c)
        indices = parse_selection(c, len(videos))
        if indices is None:
            print("&#10060; 无效输入,请重新输入")
            continue
        selected = []
        for idx in indices:
            v = videos[idx]
            p = os.path.join(output_dir, v)
            name, ext = os.path.splitext(v)
            selected.append((v, p, name, ext))
        print(f"\n已选中 {len(selected)} 个视频:")
        for v, _, _, _ in selected:
            print(f"  - {v}")
        return selected

# ==============================================
# 全局帧率记忆
# ==============================================
def input_custom_fps(video_path=None):
    global last_custom_fps, _global_remembered_fps
    
    if video_path:
        real_fps = get_video_fps_display(video_path)
        print(f"&#128073; 原生帧率:{real_fps}fps")
    
    if _global_remembered_fps is not None:
        print(f"&#9989; 沿用已设置的帧率:{_global_remembered_fps}fps")
        u = input("自定义抽帧帧率(1-120):").strip()
        check_exit(u)
        
        if not u:
            last_custom_fps = _global_remembered_fps
            return _global_remembered_fps
        
        if u.isdigit() and 1 <= int(u) <= 120:
            new_fps = int(u)
            _global_remembered_fps = new_fps
            last_custom_fps = new_fps
            print(f"&#9989; 帧率已更新为:{new_fps}fps")
            return new_fps
        else:
            print("&#10060; 仅1-120纯数字,保持原帧率")
            return _global_remembered_fps
    
    while True:
        u = input("自定义抽帧帧率(1-120):").strip()
        check_exit(u)
        
        if not u and last_custom_fps is not None:
            _global_remembered_fps = last_custom_fps
            print(f"&#128073; 已自动沿用上次帧率:{last_custom_fps}fps")
            return last_custom_fps
        
        if u.isdigit() and 1 <= int(u) <= 120:
            new_fps = int(u)
            _global_remembered_fps = new_fps
            last_custom_fps = new_fps
            print(f"&#9989; 帧率已设置为:{new_fps}fps")
            return new_fps
        
        print("&#10060; 仅1-120纯数字,重新输入:")

# ==============================================
# FFmpeg截帧(★ 关键修正:使用原始帧间隔 native_step)
# ==============================================
def extract_frames_ffmpeg(video_path, base_name, out_fps, start_sec=0, target_path=None, pix_fmt=None):
    """
    无论 out_fps 为何值,文件名时间戳始终按原始视频的帧间隔递增(与原始帧时刻对齐)
    """
    global _first_frame_extract_flag

    # 获取原始视频的帧间隔(时间步长)
    fps_num, fps_den = get_fps_fraction(video_path)
    native_step = fps_den / fps_num          # 原始每帧的时间增量

    duration = get_video_duration(video_path)
    if duration <= 0:
        print(f"&#10060; 无法获取视频时长: {video_path}")
        return False

    if pix_fmt is None:
        params = probe_color_params(video_path)
        pix_fmt = params.get("pix_fmt", "yuv420p") if params else "yuv420p"

    color_offset = None
    if target_path:
        color_offset = build_color_match_filter(video_path, target_path)

    out_dir = os.path.join(frame_dir, base_name)
    os.makedirs(out_dir, exist_ok=True)

    # 构建滤镜链
    filter_parts = [f"fps={out_fps}"]
    if color_offset:
        b_offset, g_offset, r_offset = color_offset
        filter_parts.append(f"lutrgb=r='val+{r_offset}':g='val+{g_offset}':b='val+{b_offset}'")
    filter_parts.append("scale=out_range=pc:in_range=tv:in_color_matrix=bt709:out_color_matrix=bt709")
    filter_parts.append(f"format={pix_fmt}")
    vf_filter = ",".join(filter_parts)

    if _first_frame_extract_flag:
        print(f"&#128295; 滤镜:{vf_filter}")
        _first_frame_extract_flag = False

    temp_pattern = os.path.join(out_dir, f"temp_%06d.jpeg")
    cmd = [
        "ffmpeg",
        "-hide_banner",
        "-loglevel", "info",
        "-i", video_path,
        "-vf", vf_filter,
        "-vsync", "vfr",
        "-q:v", "1",
        "-frame_pts", "1",
        temp_pattern,
        "-y"
    ]

    print(f"&#127916; 处理片段:{base_name}  总时长:{duration:.1f}s")
    print("&#9203; 正在截帧,文件会实时生成...")
    start_time = time.time()

    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=3600)
        if result.returncode != 0:
            print(f"&#10060; FFmpeg执行失败: {result.stderr}")
            return False

        temp_files = sorted([f for f in os.listdir(out_dir) if f.startswith("temp_") and f.endswith(".jpeg")])
        if not temp_files:
            print(f"&#10060; 未生成帧图")
            return False

        frame_count = 0
        last_timestamp = start_sec

        for i, temp_file in enumerate(temp_files):
            # ★ 使用原始帧间隔计算时间戳,与 out_fps 完全无关
            timestamp = start_sec + i * native_step
            time_str = sec_to_time_llc(timestamp)
            new_filename = f"{base_name}-{time_str}.jpeg"
            old_path = os.path.join(out_dir, temp_file)
            new_path = os.path.join(out_dir, new_filename)
            os.rename(old_path, new_path)
            fast_modify_md5(new_path)
            frame_count += 1
            last_timestamp = timestamp

        cost = round(time.time() - start_time, 1)
        print(f"&#9989; {base_name} 完成!共 {frame_count} 张 | 耗时 {cost}秒")
        print(f"&#128336; 时间范围:{sec_to_time_llc(start_sec)} → {sec_to_time_llc(last_timestamp)}")
        print("&#128272; 每张图片已注入唯一签名")
        print()
        return True

    except subprocess.TimeoutExpired:
        print("&#10060; FFmpeg执行超时")
        return False
    except Exception as e:
        print(f"&#10060; 截帧过程出错: {e}")
        return False

# ==============================================
# 全部截帧模式 (qbjz)
# ==============================================
def qbjz_mode(video_path, file_name, target_path=None):
    print("\n" + "=" * 40)
    print("     &#127919; 全部截帧模式 (qbjz) - 帧率抽帧")
    print("=" * 40)

    duration = get_video_duration(video_path)
    a, b = get_fps_fraction(video_path)
    real_fps = a / b
    print(f"视频:{file_name}  时长:{duration:.2f}秒  原生帧率:{real_fps:.2f}fps")

    base_name = os.path.splitext(file_name)[0]
    params = probe_color_params(video_path)
    pix_fmt = params.get("pix_fmt", "yuv420p") if params else "yuv420p"

    while True:
        out_fps = input_custom_fps(video_path)
        extract_frames_ffmpeg(video_path, base_name, out_fps, 0, target_path, pix_fmt)

        u = input("\n继续截帧回车 | n重新选片:").strip().lower()
        check_exit(u)
        if u == 'n':
            break

# ==============================================
# 批量截帧(用于切割后的片段)
# ==============================================
def _run_batch_ffmpeg(out_fps, target_path, pix_fmt=None):
    global clip_history_list
    total_clips = len(clip_history_list)

    # clip_history_list: (clip_path, start_sec, dur, out_base)
    for clip_path, start_sec, dur, out_base in clip_history_list:
        extract_frames_ffmpeg(clip_path, out_base, out_fps, start_sec, target_path, pix_fmt)

    print(f"\n&#127881; 全部完成!共 {total_clips} 个片段")
    clip_history_list.clear()

def batch_extract_frames(target_path=None):
    global clip_history_list, last_custom_fps
    if not clip_history_list:
        print("&#9888;&#65039; 暂无已分割的视频片段")
        return
    first_clip_path = clip_history_list[0][0]
    src_params_first = show_color_info(first_clip_path)
    if src_params_first is None:
        return
    out_fps = _get_fps(first_clip_path)
    pix_fmt = src_params_first.get("pix_fmt", "yuv420p")
    _run_batch_ffmpeg(out_fps, target_path, pix_fmt)

def _get_fps(video_path):
    global last_custom_fps
    custom_fps = input_custom_fps(video_path)
    if custom_fps:
        last_custom_fps = custom_fps
        return custom_fps
    a, b = get_fps_fraction(video_path)
    return a / b

# ==============================================
# 视频自动编号逻辑
# ==============================================
def find_max_existing_number(prefix, file_ext):
    if not os.path.exists(output_dir):
        return 0
    max_num = 0
    escaped_prefix = re.escape(prefix)
    escaped_ext = re.escape(file_ext)
    pattern = re.compile(f'^{escaped_prefix}(\\d+){escaped_ext}$')
    for f in os.listdir(output_dir):
        match = pattern.match(f)
        if match:
            num = int(match.group(1))
            if num > max_num:
                max_num = num
    return max_num

def get_next_output_filename(file_name, file_ext):
    if file_name.endswith("-"):
        base = file_name.rstrip("-")
        max_num = find_max_existing_number(base + "-", file_ext)
        num = max_num + 1
        out_filename = f"{base}-{num:02d}{file_ext}"
        out_path = os.path.join(output_dir, out_filename)
        return out_filename, out_path
    num_reg = re.compile(r'(\d+)$')
    num_match = num_reg.search(file_name)
    if num_match:
        prefix = file_name[:num_match.start()]
        original_num_str = num_match.group(1)
        digit_len = len(original_num_str)
        max_num = find_max_existing_number(prefix, file_ext)
        current_num = max(1, max_num + 1)
        original_num = int(original_num_str)
        current_num = max(current_num, original_num)
        out_filename = f"{prefix}{current_num:0{digit_len}d}{file_ext}"
        out_path = os.path.join(output_dir, out_filename)
        return out_filename, out_path
    out_filename = f"{file_name}{file_ext}"
    out_path = os.path.join(output_dir, out_filename)
    if os.path.exists(out_path):
        num = 1
        while True:
            out_filename = f"{file_name} {num:02d}{file_ext}"
            out_path = os.path.join(output_dir, out_filename)
            if not os.path.exists(out_path):
                break
            num += 1
    return out_filename, out_path

# ====================== qbjz扫描全部目录 ======================
def qbjz_scan_input_dir_and_process(target_path):
    all_videos = []
    for root, dirs, files in os.walk(input_dir):
        for f in files:
            fp = os.path.join(root, f)
            if f.lower().endswith(video_extensions):
                all_videos.append(fp)
    all_videos.sort()

    if not all_videos:
        print("&#9888;&#65039; 目录及所有子目录中未发现视频文件。")
        return

    print("\n===== 扫描到的全部视频(含子目录) =====")
    for i, v in enumerate(all_videos):
        print(f"{i+1}. {os.path.basename(v)}")

    while True:
        c = input("\n输入选择(支持多选,1-3 或 1,3,5)| tc退出:").strip()
        check_exit(c)
        if c.lower() == "tc":
            return
        indices = parse_selection(c, len(all_videos))
        if indices is None:
            print("&#10060; 无效输入,请重新输入")
            continue
        selected_paths = [all_videos[i] for i in indices]
        print(f"\n已选中 {len(selected_paths)} 个视频")
        break

    if len(selected_paths) == 1:
        v_path = selected_paths[0]
        v_name = os.path.basename(v_path)
        qbjz_mode(v_path, v_name, target_path)
    else:
        first_video_path = selected_paths[0]
        out_fps = input_custom_fps(first_video_path)
        for v_path in selected_paths:
            v_name = os.path.basename(v_path)
            base_name = os.path.splitext(v_name)[0]
            print(f"\n&#127916; 正在处理:{v_name}")
            extract_frames_ffmpeg(v_path, base_name, out_fps, 0, target_path)
        print(f"\n&#127881; 全部 {len(selected_paths)} 个视频截帧完成!")

# ====================== 主程序 ======================
if __name__ == "__main__":
    os.makedirs(input_dir, exist_ok=True)
    os.makedirs(output_dir, exist_ok=True)
    os.makedirs(frame_dir, exist_ok=True)

    if not check_ffmpeg():
        print("&#10060; 未检测到FFmpeg")
        input("回车退出...")
        exit()

    print("=" * 50)
    print("  视频分割 + FFmpeg 截帧 v10.0 (原始帧间隔命名)")
    print("=" * 50)

    target_path = setup_color_target()
    if target_path:
        print(f"&#127912; 色彩匹配已自动启用 → {os.path.basename(target_path)}")
    else:
        print("未检测到色彩目标,色彩匹配关闭")

    print("=" * 50)

    while True:
        _, video_path, file_name, file_ext = select_video(target_path)
        total_duration = get_video_duration(video_path)
        while True:
            start = input("\n开始时间: ").strip()
            check_exit(start)

            if start.lower() == "qbjz":
                selected_videos = select_video_from_output()
                if not selected_videos:
                    print("返回视频选择...")
                    break
                if len(selected_videos) == 1:
                    v_name, v_path, v_base, v_ext = selected_videos[0]
                    qbjz_mode(v_path, v_name, target_path)
                else:
                    print(f"\n&#128203; 将对 {len(selected_videos)} 个视频统一截帧")
                    first_video_path = selected_videos[0][1]
                    out_fps = input_custom_fps(first_video_path)
                    params_first = probe_color_params(first_video_path)
                    pix_fmt = params_first.get("pix_fmt", "yuv420p") if params_first else "yuv420p"
                    if _first_frame_extract_flag:
                        print(f"&#128295; 滤镜:fps={out_fps},scale=out_range=pc:in_range=tv:in_color_matrix=bt709:out_color_matrix=bt709,format={pix_fmt}")
                        _first_frame_extract_flag = False
                    for v_name, v_path, v_base, v_ext in selected_videos:
                        print(f"\n&#127916; 正在处理:{v_name}")
                        extract_frames_ffmpeg(v_path, v_base, out_fps, 0, target_path, pix_fmt)
                    print(f"\n&#127881; 全部 {len(selected_videos)} 个视频截帧完成!")
                break

            if start.lower() == "n":
                print("\n&#128260; 切换视频...")
                break
            if start == "":
                if clip_history_list:
                    batch_extract_frames(target_path)
                    while True:
                        u = input("\n回车继续分割视频 | tc=退出:").strip().lower()
                        check_exit(u)
                        if u == "":
                            break
                        else:
                            print("&#9888;&#65039; 回车继续 / tc退出")
                    break
                else:
                    print("&#9888;&#65039; 请先输入时间范围")
                    continue
            if not validate_time(start):
                print("&#10060; 无效输入,请使用 MM:SS / HH:MM:SS 或纯数字格式")
                continue

            s_sec = time_to_sec(start)
            if s_sec < 0:
                print("&#10060; 时间格式错误")
                continue
            if s_sec >= total_duration:
                print("&#10060; 开始时间超出视频长度")
                continue

            end = input("结束时间: ").strip()
            check_exit(end)
            if end.lower() == "n":
                print("\n&#128260; 切换视频...")
                break
            if end == "":
                if clip_history_list:
                    batch_extract_frames(target_path)
                    while True:
                        u = input("\n回车继续分割视频 | tc=退出:").strip().lower()
                        check_exit(u)
                        if u == "":
                            break
                        else:
                            print("&#9888;&#65039; 回车继续 / tc退出")
                    break
                else:
                    print("&#9888;&#65039; 尚无已分割的片段,请输入正确的结束时间进行切割")
                    continue

            if not validate_time(end):
                print("&#10060; 无效的结束时间格式")
                continue

            e_sec = time_to_sec(end)
            if e_sec <= s_sec:
                print("&#10060; 结束时间必须大于开始时间")
                continue
            dur = e_sec - s_sec

            out_filename, out_path = get_next_output_filename(file_name, file_ext)
            print(f"\n&#9986;&#65039; 正在切割:{out_filename}")
            cmd = f'ffmpeg -hide_banner -loglevel error -ss {start} -i "{video_path}" -t {dur} -c copy -movflags +faststart -y "{out_path}"'
            if subprocess.run(cmd, shell=True).returncode == 0:
                if os.path.exists(out_path) and os.path.getsize(out_path) > 1024:
                    print("&#9989; 切割完成")
                    out_base = os.path.splitext(out_filename)[0]
                    # ★ 存储片段信息,包含起始时间
                    clip_history_list.append((out_path, s_sec, dur, out_base))
                    print(f"&#128203; 累计片段:{len(clip_history_list)} → {out_base}")
                else:
                    print("&#10060; 切割失败:输出文件异常")
            else:
                print("&#10060; 切割失败")
改一下吗&#129401;
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-8-22 23:47

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表