视频相关处理

获取视频的分辨率

1
2
3
4
5
ffprobe -v error \
  -select_streams v:0 \
  -show_entries stream=width,height \
  -of csv=p=0 \
  2-PNA.mp4

ffprobe是通过homebrew安装的,它会输出视频的分辨率。

将视频转化成音频

ffmpeg -i input_video.mp4 -vn -acodec mp3 output_audio.mp3

获取视频帧

ffmpeg -i 视频路径 -vf “fps=1” 输出的图片路径

yt-dlp:视频下载

  • 它是通过python的pip工具进行安装。官网:https://github.com/yt-dlp/yt-dlp,关于options可以看介绍,以及在python中如何配置参数,可以看:https://github.com/yt-dlp/yt-dlp/blob/master/yt_dlp/options.py
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    import yt_dlp
    url = "***"# 指定要下载的url,如果是一个合集的url,在下载的时候没有指定某一集,会下载所有的集数
    ydl_opts = {
        'playlist_items': '14',#指定下载某一集,也可以指定下载范围,只有url为合集的时候才起作用
        'cookiefile': 'cookies.txt',#指定cookie文件,一般google浏览器通过安装get cookies.txt插件,方便下载会员视频
        'outtmpl': '要下载的父文件夹/%(playlist_index)02d - %(title)s.%(ext)s',#指定下载的路径和名字
    
        'format': 'bestvideo+bestaudio/best',#指定下载的视频分辨率和音频的采样率,这里如果音频和视频分开的,可以只下载音频或视频
    
        'concurrent_fragment_downloads': 8,#指定单集下载的线程个数
    
    
        'ignoreerrors': True,#是否忽略下载失败后继续下载
        'merge_output_format': 'mp4',#指定音频和视频合并后的视频类型
    }
    with yt_dlp.YoutubeDL(ydl_opts) as ydl:
        ydl.download([url])
    
  • yt-dlp -F url:可以查看资源支持哪些格式,有些视频不支持视频和音频分开。

水印处理(Python+OpenCV)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
import cv2
import numpy as np
from pathlib import Path

def get_watermark_coordinates(video_file, frame_number=0):
    """
    交互式获取水印坐标
    
    参数:
        video_file: 视频文件路径
        frame_number: 要分析的帧号(默认第一帧)
    
    返回:
        (x, y, width, height) 或 None
    """
    cap = cv2.VideoCapture(video_file)
    
    if not cap.isOpened():
        print(f"❌ 无法打开视频: {video_file}")
        return None
    
    # 跳转到指定帧
    cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number)
    ret, frame = cap.read()
    cap.release()
    
    if not ret:
        print("❌ 无法读取帧")
        return None
    
    # 显示视频信息
    total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
    fps = cap.get(cv2.CAP_PROP_FPS)
    width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
    height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
    
    print(f"视频信息: {width}x{height}, {total_frames}帧, {fps:.2f}fps")
    print(f"当前帧: {frame_number}/{total_frames}")
    print("\n操作说明:")
    print("1. 在窗口中拖拽鼠标选择水印区域")
    print("2. 按 Enter 确认")
    print("3. 按 ESC 取消")
    
    # 选择 ROI(Region of Interest)
    roi = cv2.selectROI('选择水印区域 (按 Enter 确认, ESC 取消)', frame, False)
    cv2.destroyAllWindows()
    
    if roi[2] > 0 and roi[3] > 0:
        x, y, w, h = roi
        print(f"\n✅ 水印坐标:")
        print(f"   x (左上角X): {x}")
        print(f"   y (左上角Y): {y}")
        print(f"   width (宽度): {w}")
        print(f"   height (高度): {h}")
        print(f"\nFFmpeg 命令:")
        print(f'   ffmpeg -i "{video_file}" -vf "delogo=x={x}:y={y}:w={w}:h={h}" output.mp4')
        
        return (x, y, w, h)
    else:
        print("❌ 未选择区域")
        return None


def get_multiple_watermarks(video_file, frame_number=0):
    """
    选择多个水印区域
    """
    cap = cv2.VideoCapture(video_file)
    cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number)
    ret, frame = cap.read()
    cap.release()
    
    if not ret:
        return []
    
    watermarks = []
    frame_copy = frame.copy()
    
    print("选择水印区域(可以多次选择)")
    print("按 Enter 确认当前选择,按 ESC 完成所有选择")
    
    while True:
        roi = cv2.selectROI('选择水印区域 (Enter确认, ESC完成)', frame_copy, False)
        
        if roi[2] > 0 and roi[3] > 0:
            x, y, w, h = roi
            watermarks.append((x, y, w, h))
            
            # 在图像上标记已选择的区域
            cv2.rectangle(frame_copy, (x, y), (x+w, y+h), (0, 255, 0), 2)
            cv2.putText(frame_copy, f"#{len(watermarks)}", (x, y-10), 
                    cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
            
            print(f"✅ 已选择水印 #{len(watermarks)}: x={x}, y={y}, w={w}, h={h}")
        else:
            break
    
    cv2.destroyAllWindows()
    
    return watermarks


# 使用示例
if __name__ == "__main__":
    video_file = "input.mp4"
    
    # 获取单个水印坐标
    coords = get_watermark_coordinates(video_file, frame_number=0)
    
    # 获取多个水印坐标
    # watermarks = get_multiple_watermarks(video_file)
    # for i, (x, y, w, h) in enumerate(watermarks, 1):
    #     print(f"水印 {i}: x={x}, y={y}, w={w}, h={h}")

执行上面的python后,接着会弹出交互式界面,此时选中水印,然后就会输出坐标信息:

1
2
3
4
5
6
7
8
 水印坐标:
x (左上角X): 1115
y (左上角Y): 45
width (宽度): 333
height (高度): 77

FFmpeg 命令:
ffmpeg -i "input.mp4" -vf "delogo=x=1115:y=45:w=333:h=77" output.mp4

接着我们执行该ffmpeg命令就行,会输出不带水印的视频。

批量去水印

如果想给某个文件夹中的视频批量去水印,可以使用如下案例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import subprocess
from pathlib import Path

# ===== 配置区 =====
INPUT_DIR = Path("你要去水印的目录")
OUTPUT_DIR = Path("输出去水印的视频目录")

DELOGO_FILTER = "delogo=x=2505:y=108:w=338:h=130"

VIDEO_EXTS = {".mp4", ".mkv", ".mov", ".avi"}  # 支持的视频格式
# =================

def remove_logo(input_video: Path, output_video: Path):
    cmd = [
        "ffmpeg",
        "-y",                     # 覆盖输出文件
        "-i", str(input_video),
        "-vf", DELOGO_FILTER,
        "-c:a", "copy",           # 音频不重新编码(更快)
        str(output_video)
    ]

    print(f"🎬 处理: {input_video.name}")
    subprocess.run(cmd, check=True)


def main():
    OUTPUT_DIR.mkdir(parents=True, exist_ok=True)

    videos = [
        p for p in INPUT_DIR.iterdir()
        if p.suffix.lower() in VIDEO_EXTS and p.is_file()
    ]

    if not videos:
        print("❌ 未找到视频文件")
        return

    for video in sorted(videos):
        output_path = OUTPUT_DIR / video.name
        remove_logo(video, output_path)

    print("✅ 全部视频处理完成")


if __name__ == "__main__":
    main()

视频提取原声/BGM/去掉人声等

首先需要安装demucs的虚拟环境,安装虚拟环境需要用到pyenv,首先安装pyenv:

1
brew install pyenv

然后录配置pyenv的path路径,在zshrc或bash中配置:

1
2
3
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"

然后同步下配置:source ~/.zshrc。 然后安装虚拟环境的python版本:

1
pyenv install 3.10.14

接着需要安装pyenv-virtualenv用于指定虚拟环境的名字:

1
brew install pyenv-virtualenv

然后给pyenv-virtualenv添加上环境变量:

1
eval "$(pyenv virtualenv-init -)"

接着指定虚拟环境的名字和激活虚拟环境:

1
2
pyenv virtualenv 3.10.14 demucs-env\
pyenv activate demucs-env

此时就激活了虚拟环境demucs-env,此时在命令行中能看到虚拟环境的名字,如果要断开虚拟环境使用:pyenv deactivate demucs-env。 接着要装相关的依赖组件,分别是:

1
2
3
4
pip install numpy==1.26.4
pip install torch==2.2.2 torchaudio==2.2.2
pip install demucs
pip install torchcodec

如果终端还是识别不了,可以刷新下终端:

1
hash -r

此时还需要ffmpeg,这个可以通过brew install ffmpeg进行安装。所有安装完成后,最后同步下命令:hash -r。这样python的虚拟环境就安装完成了。 此时可以通过python3 video_audio_example.py 2去执行各种功能。功能都在两个python事例文件python功能文件中。

常用demucs的命令:

1
demucs --two-stems=vocals 音频路径

此时会分离出音频的人声和bgm。

图片模糊处理

目前已知的通过可视化添加模糊效果的有tkinter工具,具体可见下面的实现:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
import tkinter as tk
from tkinter import filedialog, messagebox, Scale
from PIL import Image, ImageTk, ImageFilter, ImageDraw
import numpy as np


class BlurRegionGUI:
    """可视化模糊区域选择 GUI"""

    def __init__(self, root):
        self.root = root
        self.root.title("图片区域模糊工具")
        self.root.geometry("1200x800")

        self.image_path = None
        self.original_image = None
        self.display_image = None
        self.regions = []  # [(x1, y1, x2, y2), ...]
        self.current_region = None
        self.start_x = None
        self.start_y = None
        self.blur_radius = 15

        self.setup_ui()

    def setup_ui(self):
        """设置UI界面"""
        # 顶部按钮栏
        button_frame = tk.Frame(self.root)
        button_frame.pack(pady=10)

        tk.Button(button_frame, text="打开图片", command=self.load_image,
                  width=15, height=2).pack(side=tk.LEFT, padx=5)
        tk.Button(button_frame, text="应用模糊", command=self.apply_blur,
                  width=15, height=2).pack(side=tk.LEFT, padx=5)
        tk.Button(button_frame, text="重置", command=self.reset_image,
                  width=15, height=2).pack(side=tk.LEFT, padx=5)
        tk.Button(button_frame, text="保存", command=self.save_image,
                  width=15, height=2).pack(side=tk.LEFT, padx=5)
        tk.Button(button_frame, text="清除选择", command=self.clear_regions,
                  width=15, height=2).pack(side=tk.LEFT, padx=5)

        # 模糊半径调节
        radius_frame = tk.Frame(self.root)
        radius_frame.pack(pady=5)
        tk.Label(radius_frame, text="模糊半径:").pack(side=tk.LEFT, padx=5)
        self.radius_scale = Scale(radius_frame, from_=1, to=50, orient=tk.HORIZONTAL,
                                  length=200, command=self.update_blur_radius)
        self.radius_scale.set(15)
        self.radius_scale.pack(side=tk.LEFT, padx=5)
        self.radius_label = tk.Label(radius_frame, text="15")
        self.radius_label.pack(side=tk.LEFT, padx=5)

        # 说明文字
        info_label = tk.Label(self.root,
                              text="操作说明: 鼠标左键拖拽选择区域,可以多次选择多个区域",
                              font=("Arial", 10))
        info_label.pack(pady=5)

        # 图片显示区域
        canvas_frame = tk.Frame(self.root)
        canvas_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

        self.canvas = tk.Canvas(canvas_frame, bg="gray", cursor="crosshair")
        self.canvas.pack(fill=tk.BOTH, expand=True)

        # 绑定鼠标事件
        self.canvas.bind("<Button-1>", self.on_mouse_press)
        self.canvas.bind("<B1-Motion>", self.on_mouse_drag)
        self.canvas.bind("<ButtonRelease-1>", self.on_mouse_release)

    def update_blur_radius(self, value):
        """更新模糊半径"""
        self.blur_radius = int(float(value))
        self.radius_label.config(text=str(self.blur_radius))

    def load_image(self):
        """加载图片"""
        file_path = filedialog.askopenfilename(
            filetypes=[("output_frame_0001.png", "*.jpg *.jpeg *.png *.bmp *.gif")]
        )
        if file_path:
            self.image_path = file_path
            self.original_image = Image.open(file_path)
            self.display_image = self.original_image.copy()
            self.regions = []
            self.display_image_on_canvas()

    def display_image_on_canvas(self):
        """在画布上显示图片"""
        if self.display_image is None:
            return

        # 获取画布大小
        canvas_width = self.canvas.winfo_width()
        canvas_height = self.canvas.winfo_height()

        if canvas_width <= 1 or canvas_height <= 1:
            self.root.after(100, self.display_image_on_canvas)
            return

        # 计算缩放比例
        img_width, img_height = self.display_image.size
        scale = min(canvas_width / img_width, canvas_height / img_height)

        # 缩放图片
        display_width = int(img_width * scale)
        display_height = int(img_height * scale)
        display_img = self.display_image.resize((display_width, display_height), Image.Resampling.LANCZOS)

        # 转换为 PhotoImage
        self.photo = ImageTk.PhotoImage(display_img)

        # 清除画布并显示图片
        self.canvas.delete("all")
        self.canvas.create_image(canvas_width // 2, canvas_height // 2,
                                 image=self.photo, anchor=tk.CENTER)

        # 绘制已选择的区域
        for x1, y1, x2, y2 in self.regions:
            # 转换坐标到显示坐标
            offset_x = (canvas_width - display_width) // 2
            offset_y = (canvas_height - display_height) // 2
            self.canvas.create_rectangle(
                x1 * scale + offset_x, y1 * scale + offset_y,
                x2 * scale + offset_x, y2 * scale + offset_y,
                outline="green", width=2
            )

    def on_mouse_press(self, event):
        """鼠标按下"""
        self.start_x = event.x
        self.start_y = event.y

    def on_mouse_drag(self, event):
        """鼠标拖拽"""
        if self.start_x is None or self.start_y is None:
            return

        # 清除当前临时矩形
        self.canvas.delete("temp_rect")

        # 绘制临时矩形
        self.canvas.create_rectangle(
            self.start_x, self.start_y, event.x, event.y,
            outline="red", width=2, tags="temp_rect"
        )

    def on_mouse_release(self, event):
        """鼠标释放"""
        if self.start_x is None or self.start_y is None:
            return

        if self.display_image is None:
            return

        # 获取画布和图片信息
        canvas_width = self.canvas.winfo_width()
        canvas_height = self.canvas.winfo_height()
        img_width, img_height = self.display_image.size

        scale = min(canvas_width / img_width, canvas_height / img_height)
        offset_x = (canvas_width - int(img_width * scale)) // 2
        offset_y = (canvas_height - int(img_height * scale)) // 2

        # 转换坐标到图片坐标
        x1 = int((self.start_x - offset_x) / scale)
        y1 = int((self.start_y - offset_y) / scale)
        x2 = int((event.x - offset_x) / scale)
        y2 = int((event.y - offset_y) / scale)

        # 确保坐标在图片范围内
        x1 = max(0, min(x1, img_width))
        y1 = max(0, min(y1, img_height))
        x2 = max(0, min(x2, img_width))
        y2 = max(0, min(y2, img_height))

        # 确保 x1 < x2, y1 < y2
        if x1 > x2:
            x1, x2 = x2, x1
        if y1 > y2:
            y1, y2 = y2, y1

        # 检查区域是否有效
        if abs(x2 - x1) > 5 and abs(y2 - y1) > 5:
            self.regions.append((x1, y1, x2, y2))
            print(f"已选择区域: ({x1}, {y1}) -> ({x2}, {y2})")
            self.display_image_on_canvas()

        self.start_x = None
        self.start_y = None

    def apply_blur(self):
        """应用模糊效果"""
        if self.original_image is None:
            messagebox.showwarning("警告", "请先加载图片!")
            return

        if not self.regions:
            messagebox.showwarning("警告", "请先选择要模糊的区域!")
            return

        # 创建模糊版本
        blurred = self.original_image.filter(ImageFilter.GaussianBlur(radius=self.blur_radius))

        # 创建遮罩
        mask = Image.new('L', self.original_image.size, 0)
        draw = ImageDraw.Draw(mask)

        # 绘制所有选择的区域
        for x1, y1, x2, y2 in self.regions:
            draw.rectangle([x1, y1, x2, y2], fill=255)

        # 合成
        self.display_image = Image.composite(blurred, self.original_image, mask)
        self.regions = []  # 清空选择
        self.display_image_on_canvas()
        messagebox.showinfo("成功", "模糊效果已应用!")

    def reset_image(self):
        """重置图片"""
        if self.original_image:
            self.display_image = self.original_image.copy()
            self.regions = []
            self.display_image_on_canvas()

    def clear_regions(self):
        """清除所有选择"""
        self.regions = []
        self.display_image_on_canvas()

    def save_image(self):
        """保存图片"""
        if self.display_image is None:
            messagebox.showwarning("警告", "没有可保存的图片!")
            return

        file_path = filedialog.asksaveasfilename(
            defaultextension=".jpg",
            filetypes=[("JPEG", "*.jpg"), ("PNG", "*.png"), ("所有文件", "*.*")]
        )
        if file_path:
            self.display_image.save(file_path)
            messagebox.showinfo("成功", f"图片已保存到: {file_path}")


# 使用示例
if __name__ == '__main__':
    root = tk.Tk()
    app = BlurRegionGUI(root)
    root.mainloop()

ncm转MP3

https://ncm.worthsee.com/

Licensed under CC BY-NC-SA 4.0
使用 Hugo 构建
主题 StackJimmy 设计