rootcup 发表于 2024-9-22 05:59

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

本帖最后由 rootcup 于 2024-9-22 06:05 编辑

PySceneDetect是一款基于opencv的视频场景切换检测和分析工具。本文代码也是利用pyscenedetect实现类似的功能

剪映的智能镜头分割
https://img.meituan.net/csc/40cfa1bd5f48df0033ad65fb0638b9c9280486.jpg

达芬奇的场景切割
https://img.meituan.net/csc/2c8ed306353603f8c7e67815fc224965383560.jpg

均能实现自动识别镜头切换,然后分割


文末也附上打包好的exe程序
打包后如下,由于需要用到ffmpeg,顺带在根目录放一个ffmpeg.exe方便程序调用

https://img.meituan.net/csc/af6d93f978011579d68624205e2b99d990057.jpg

图形界面如下

https://img.meituan.net/csc/8aba4ffc53786f0a514e3db5d3653d2e226210.jpg

可以选择单个视频文件,也可选择视频文件夹批量处理

https://img.meituan.net/csc/52b76f2097cbd7388dc34c074b2cb884366967.jpg

https://img.meituan.net/csc/1458e66c3294f1c73955c9bc2ac30f3c1206819.jpg

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['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['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['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['title'])
    btn_about.config(text=languages['about'])
    btn_select_file.config(text=languages['select_video_file'])
    btn_select_folder.config(text=languages['select_video_folder'])
    label_threshold.config(text=languages['threshold'])
    btn_start.config(text=languages['start'])
    btn_open_folder.config(text=languages['open_folder'])
    text_video_paths.delete(1.0, tk.END)
    text_video_paths.insert(tk.END, languages['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['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['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['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['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['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 = )]
    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['select_error'], languages['select_error_message'])
      return

    try:
      threshold = float(entry_threshold.get())
      config['threshold'] = threshold
      save_config(config)
    except ValueError:
      messagebox.showerror(languages['threshold_error'],
                           languages['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 =
      for future in concurrent.futures.as_completed(futures):
            try:
                future.result()
            except Exception as e:
                messagebox.showerror(languages['select_error'],
                                     f"{languages['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))
    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['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))
      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['about'])
    about_window.geometry("300x300")

    # 图片和介绍
    info_label = tk.Label(about_window, text=languages['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 发表于 2024-9-24 11:00

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

壹个小菜鸡 发表于 2024-9-27 17:54

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

wkdxz 发表于 2024-9-22 10:56

很不错,让我们可以免费使用VIP功能

文西思密达 发表于 2024-9-22 11:02

这个 功能很实用!

dappeng 发表于 2024-9-22 12:05

太好了,学习了

W168888 发表于 2024-9-22 13:47

感谢,很有用

SU150228 发表于 2024-9-22 15:28

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

asmtodeath 发表于 2024-9-22 16:44

学习一下

abpyu 发表于 2024-9-22 17:27

谢谢大佬分享{:1_893:}

lamquan 发表于 2024-9-22 20:00

使用TransNetV2用CUDA加速,更快更好

rootcup 发表于 2024-9-22 22:42

lamquan 发表于 2024-9-22 20:00
使用TransNetV2用CUDA加速,更快更好

感谢,我去看看
页: [1] 2
查看完整版本: 利用pyscenedetect实现类似剪映智能镜头分割