利用pyscenedetect实现类似剪映智能镜头分割

查看 101|回复 11
作者:rootcup   
PySceneDetect是一款基于opencv的视频场景切换检测和分析工具。本文代码也是利用pyscenedetect实现类似的功能
剪映的智能镜头分割
[i]
达芬奇的场景切割
[i]
均能实现自动识别镜头切换,然后分割
文末也附上打包好的exe程序
打包后如下,由于需要用到ffmpeg,顺带在根目录放一个ffmpeg.exe方便程序调用
[i]
图形界面如下
[i]
可以选择单个视频文件,也可选择视频文件夹批量处理
[i]
[i]
[Python] 纯文本查看 复制代码import base64
import concurrent.futures
import io
import json
import os
import subprocess
import threading
import tkinter as tk
from ctypes import windll
from tkinter import filedialog, messagebox, Toplevel
from PIL import Image, ImageTk
from scenedetect import open_video, SceneManager
from scenedetect.detectors import ContentDetector
from scenedetect.video_splitter import split_video_ffmpeg
# 设置应用程序的默认DPI,确保高DPI屏幕上的显示效果
windll.shcore.SetProcessDpiAwareness(2)
font = 'simhei.ttf'  # 设置字体 win自带的,不用绝对路径
config_file = 'config.json'
# 多语言支持
languages = {
    'zh': {
        'title': '分镜探测切割 1.4',
        'about': '关于',
        'select_video_file': '选择视频文件',
        'select_video_folder': '选择视频文件夹',
        'threshold': '画面变化阈值:',
        'start': '开始切割分镜',
        'open_folder': '打开输出文件夹',
        'readme': "2024年9月22日 by 吾爱破解:rootcup\n说明:画面变化阈值 数字越小越灵敏,切割的分镜头就越多。\n程序调用Py scenedetect,实现类似达芬奇Resolve和剪映的场景剪切探测/智能镜头分割。\n选择视频文件,是单文件操作。选择视频文件夹,是批量操作。",
        'select_error': '错误',
        'select_error_message': '请选择一个视频文件或文件夹.',
        'threshold_error': '错误',
        'threshold_error_message': '阈值必须是一个有效的数字.',
        'completion_message': '视频分割已完成!分镜视频位于 {} 文件夹内。\n',
        'about_info': '感谢使用本程序!\n'
    },
    'en': {
        'title': 'Scene Detection and Splitting 1.3',
        'about': 'About',
        'select_video_file': 'Select Video File',
        'select_video_folder': 'Select Video Folder',
        'threshold': 'Scene Change Threshold:',
        'start': 'Start Splitting Scenes',
        'open_folder': 'Open Output Folder',
        'readme': 'September 22, 2024 52pj: rootcup\n Description: The smaller and more sensitive the number of screen change threshold, the more the shot will be cut. The program calls Py scenedetect to implement scene clipping detection/smart shot segmentation similar to Da Vinci Resolve and clipping. \n Select video file, is a single file operation. Select the video folder in batches',
        'select_error': 'Error',
        'select_error_message': 'Please select a video file or folder.',
        'threshold_error': 'Error',
        'threshold_error_message': 'Threshold must be a valid number.',
        'completion_message': 'Video splitting is complete! The split scenes are located in the {} folder.\n',
        'about_info': 'Thank you for using this program!\n'
    }
}
current_lang = 'zh'  # 当前语言设置
# 加载配置
def load_config():
    if os.path.exists(config_file):
        with open(config_file, 'r', encoding='utf-8') as file:
            config = json.load(file)
            return config
    return {'threshold': 27, 'last_folder': '', 'language': 'zh'}
# 保存配置
def save_config(config):
    with open(config_file, 'w', encoding='utf-8') as file:
        json.dump(config, file, ensure_ascii=False, indent=4)
config = load_config()
current_lang = config.get('language', 'zh')
# 设置应用程序的默认DPI,确保高DPI屏幕上的显示效果
windll.shcore.SetProcessDpiAwareness(2)  # DPI_AWARENESS_PER_MONITOR_V2
font = 'simhei.ttf'
readme = languages[current_lang]['readme']
# 52logo图片的Base64编码
jpg1_base64 = '/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAFA3PEY8MlBGQUZaVVBfeMiCeG5uePWvuZHI////////////////////////////////////////////////////2wBDAVVaWnhpeOuCguv/////////////////////////////////////////////////////////////////////////wAARCAAwADADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwBZGkMxCtjngBqX9+Sfm+vzDihQvnuScdcUqoBGXG4BhjHU9fwqTYSVXQlg+QvqckU1XLZ3TMtLMd6sw3D5sYP0ohC9d3Pcbc0D6ajjhSVNwwP0NIkjeeAHLjP+eKmydxJclf7uyqyqBMgVtwyO2KBLX+v+ADttmZl9frS+blWDDrjAHFBUNM+TgAkmnGNSynnDHAXGP8aB6DJJfMGCvToc9KI2Ck8kZGMjqKV1QqxQY2nHXrSRIHPzBueAR0pD0sOBRWDGRmI56U1DunBx1PTNSCNQASmBjJLH9O1RopSZdwxyOlMWhKozcSAE9Oo69qdIQGbcSACP5HpUbJIkjYXIbI/OhjMc/IQSc5UH6UE2uJMPkXBbG0H2ogIIKksM85Hakk85gNytgcUKrqmPLPzHr/SgroSOELgtg56Dd/gKjzm4GeMHGB7U/dKPlERx+J/WkSF/OB24AOetAlof/9k='
# 创建主窗口
root = tk.Tk()
root.title(languages[current_lang]['title'])
root.geometry('550x600')
root.configure(bg='#f0f0f0')  # 设置背景色
# 创建一个Frame用于放置关于按钮
top_frame = tk.Frame(root, bg='#f0f0f0')
top_frame.pack(side=tk.TOP, fill=tk.X)
# 在右上角添加关于按钮
btn_about = tk.Button(top_frame, text=languages[current_lang]['about'], fg='black', font=(font, 10, 'bold'),
                      relief=tk.FLAT, command=lambda: show_about_window())
btn_about.pack(side=tk.RIGHT, padx=10, pady=10)
# 创建语言切换按钮
language_menu = tk.Menubutton(top_frame, text="Language", fg='black', font=(font, 10, 'bold'), relief=tk.FLAT)
language_menu.menu = tk.Menu(language_menu, tearoff=0)
language_menu["menu"] = language_menu.menu
language_menu.menu.add_command(label="中文", command=lambda: change_language('zh'))
language_menu.menu.add_command(label="English", command=lambda: change_language('en'))
language_menu.pack(side=tk.RIGHT, padx=10, pady=10)
# 更新语言
def update_language():
    root.title(languages[current_lang]['title'])
    btn_about.config(text=languages[current_lang]['about'])
    btn_select_file.config(text=languages[current_lang]['select_video_file'])
    btn_select_folder.config(text=languages[current_lang]['select_video_folder'])
    label_threshold.config(text=languages[current_lang]['threshold'])
    btn_start.config(text=languages[current_lang]['start'])
    btn_open_folder.config(text=languages[current_lang]['open_folder'])
    text_video_paths.delete(1.0, tk.END)
    text_video_paths.insert(tk.END, languages[current_lang]['readme'], "tag_1")
# 启动按钮和打开输出文件夹按钮放在同一行的 Frame
button0_frame = tk.Frame(root, bg='#f0f0f0')
button0_frame.pack(pady=20)
# 选择文件按钮
btn_select_file = tk.Button(button0_frame, text=languages[current_lang]['select_video_file'], bg='#4CAF50', fg='white',
                            font=(font, 12, 'bold'),
                            relief=tk.FLAT, command=lambda: select_file())
btn_select_file.pack(pady=20, side=tk.LEFT, padx=10)
# 选择文件夹按钮
btn_select_folder = tk.Button(button0_frame, text=languages[current_lang]['select_video_folder'], bg='#4CAF50',
                              fg='white', font=(font, 12, 'bold'),
                              relief=tk.FLAT, command=lambda: select_folder())
btn_select_folder.pack(pady=10, side=tk.RIGHT, padx=10)
# 设置阈值标签和输入框
label_threshold = tk.Label(root, text=languages[current_lang]['threshold'], bg='#f0f0f0', font=(font, 10))
label_threshold.pack()
entry_threshold = tk.Entry(root, width=17)
entry_threshold.insert(0, str(config.get('threshold', 27)))
entry_threshold.pack(pady=5)
# 启动按钮和打开输出文件夹按钮放在同一行的 Frame
button_frame = tk.Frame(root, bg='#f0f0f0')
button_frame.pack(pady=20)
# 启动按钮
btn_start = tk.Button(button_frame, text=languages[current_lang]['start'], bg='#2196F3', fg='white',
                      font=(font, 12, 'bold'),
                      relief=tk.FLAT, command=lambda: start_detection())
btn_start.pack(side=tk.LEFT, padx=10)
# 打开输出文件夹按钮
btn_open_folder = tk.Button(button_frame, text=languages[current_lang]['open_folder'], bg='#FFC107', fg='white',
                            font=(font, 12, 'bold'),
                            relief=tk.FLAT, command=lambda: open_output_folder())
btn_open_folder.pack(side=tk.LEFT, padx=10)
# 视频路径显示文本框
text_video_paths = tk.Text(root, height=4000, width=6000, bg='#f0f0f0', font=(font, 10))
text_video_paths.pack(padx=5, pady=10)
text_video_paths.tag_config("tag_1", foreground="green")  # 设置文本颜色为绿色
text_video_paths.tag_config("tag_2", foreground="blue")
text_video_paths.insert(tk.END, readme, "tag_1")
# 全局变量
selected_folder = config.get('last_folder', '')
video_files = []
# 函数定义
def select_file():
    global video_files
    video_files = [filedialog.askopenfilename(filetypes=[("MP4文件", "*.mp4;*.MP4")])]
    if video_files:
        update_video_paths()
def select_folder():
    global selected_folder, video_files
    selected_folder = filedialog.askdirectory()
    if selected_folder:
        config['last_folder'] = selected_folder
        save_config(config)
        video_files = [os.path.join(selected_folder, f) for f in os.listdir(selected_folder) if
                       f.lower().endswith('.mp4')]
        update_video_paths()
def update_video_paths():
    text_video_paths.delete(1.0, tk.END)
    for video_file in video_files:
        text_video_paths.insert(tk.END, video_file + '\n')
def start_detection():
    if not video_files:
        messagebox.showerror(languages[current_lang]['select_error'], languages[current_lang]['select_error_message'])
        return
    try:
        threshold = float(entry_threshold.get())
        config['threshold'] = threshold
        save_config(config)
    except ValueError:
        messagebox.showerror(languages[current_lang]['threshold_error'],
                             languages[current_lang]['threshold_error_message'])
        return
    detection_thread = threading.Thread(target=run_detection, args=(threshold,))
    detection_thread.start()
def run_detection(threshold):
    with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
        futures = [executor.submit(split_video_into_scenes, video_path, threshold) for video_path in video_files]
        for future in concurrent.futures.as_completed(futures):
            try:
                future.result()
            except Exception as e:
                messagebox.showerror(languages[current_lang]['select_error'],
                                     f"{languages[current_lang]['select_error_message']} {str(e)}")
    text_video_paths.insert(tk.END, '已处理完成全部\n', "tag_1")
def split_video_into_scenes(video_path, threshold=27.0):
    base_filename = os.path.splitext(os.path.basename(video_path))[0]
    save_path = f'./分镜切割/{base_filename}/'
    if not os.path.exists(save_path):
        os.makedirs(save_path)
    output_file_template = f'{save_path}Scene-$SCENE_NUMBER.mp4'
    video = open_video(video_path)
    scene_manager = SceneManager()
    scene_manager.add_detector(ContentDetector(threshold=threshold))
    scene_manager.detect_scenes(video, show_progress=True)
    scene_list = scene_manager.get_scene_list()
    split_video_ffmpeg(video_path, scene_list, output_file_template=output_file_template, show_progress=True,
                       show_output=True)
    update_completion_message(save_path)
def update_completion_message(save_path):
    message = languages[current_lang]['completion_message'].format(save_path)
    text_video_paths.insert(tk.END, message, 'tag_2')
def open_output_folder():
    if video_files:
        base_filename = os.path.splitext(os.path.basename(video_files[0]))[0]
        save_path = f'./分镜切割/{base_filename}/'
        if os.path.exists(save_path):
            subprocess.Popen(f'explorer "{os.path.abspath(save_path)}"')
def show_about_window():
    about_window = Toplevel(root)
    about_window.title(languages[current_lang]['about'])
    about_window.geometry("300x300")
    # 图片和介绍
    info_label = tk.Label(about_window, text=languages[current_lang]['about_info'], font=(font, 12))
    info_label.pack(pady=10)
    # 显示第一张图片
    img1_data = base64.b64decode(jpg1_base64)
    img1 = Image.open(io.BytesIO(img1_data))
    img1_tk = ImageTk.PhotoImage(img1)
    img1_label = tk.Label(about_window, image=img1_tk)
    img1_label.image = img1_tk
    img1_label.pack(pady=5)
# 切换语言
def change_language(lang):
    global current_lang
    current_lang = lang
    config['language'] = current_lang
    save_config(config)
    update_language()
# 初始化语言
update_language()
# 运行主循环
root.mainloop()
打包好的exe下载:https://wwmd.lanzouv.com/i9clx2ajwunc
密码:52pj

按钮, 文件夹

hanbazhen   

楼主,能不能做个 “自动跟踪打码或者去掉”工具,镜头切换,就变位置了,剪印手动一帧一帧打太累了
壹个小菜鸡   

楼主,这功能是不是就是 那种 一个场景切换至另一个场景,然后工具从中间切分开的?分成两个视频
wkdxz   

很不错,让我们可以免费使用VIP功能
文西思密达   

这个 功能很实用!
zylz9941   

谢谢楼主的分享精神
dappeng   

太好了,学习了
W168888   

感谢,很有用
SU150228   

我有个想法:能否实现自动识别声音停顿切换
asmtodeath   

学习一下
您需要登录后才可以回帖 登录 | 立即注册

返回顶部