python烧烤吧 发表于 2026-4-5 02:48:03

视频转字符画

import cv2
import tkinter as tk
import os.path as op
from tkinter import messagebox as mgb
from tkinter import ttk
from tkinter import filedialog as fdl

def video2strs(path_, root):

    if not op.isfile(path_) or not path_.split(".") == "mp4":
      mgb.showwarning(title="Warning", messge="目前仅支持MP4格式的转换!!")
      return

    dir_name, file_name = op.split(path_)
    new_text_position = dir_name + r"/"+ file_name.split(".")

    #os.chdir(dir_name)
    _ = 0
    suffix = ".v2s"
    while op.isfile(new_text_position + suffix):
      _ += 1
      suffix = f"({_}).v2s"
    new_text_position += suffix
    print(new_text_position)

    def get_resize_height():
      _ = cv2.VideoCapture(path_)
      ret, temp_frame = _.read()
      temp_frame = cv2.resize(temp_frame, (0, 0), fx=0.09375,fy=0.046875)
      __ = temp_frame.shape
      _.release()
      return __

    def start_trans():

      text_f = open(new_text_position, "w", encoding="utf-8")
      text_f.write(str(get_resize_height()) + "\n")
      #with open(new_text_position, "w", encoding="utf-8") as text_f:

      def trans():
            nonlocal count
            if video.isOpened():
                temp_text = ""
                ret, current_frame = video.read()
                if ret:
                  current_frame = cv2.resize(cv2.cvtColor(current_frame, cv2.COLOR_BGR2GRAY), (0, 0), fx=0.09375,fy=0.046875)
                  for i in range(current_frame.shape):
                        for j in range(current_frame.shape):
                            temp_text += ascii__ / 255 * 7)]
                        temp_text += '\n'
                  else:
                        text_f.write(temp_text)
                        if count > one_of_ten:
                            progress_bar["value"] += 10
                            progress_bar.pack(pady=(1, 2))
                            count = 0
                        show_progress.after(0, trans)
                        count += 1
                        #print("+1f")
                else:
                  label1.config(text="OK!")
                  progress_bar["value"] = 100
                  progress_bar.pack(pady=(1, 2))
                  text_f.close()
                  tk.Button(show_progress, text="Quit", command=show_progress.destroy, width=5).pack(pady=15)
                  video.release()

      video = cv2.VideoCapture(path_)
      amount_of_frame = video.get(cv2.CAP_PROP_FRAME_COUNT)
      one_of_ten = amount_of_frame // 10
      temp_text = ""
      ascii__ = ["@", "G·", "¥", "·*·", "·+", "·=", "-'-", "·_·"]
      count = 0
      show_progress.after(0, trans)

    show_progress = tk.Tk()
    show_progress.geometry("300x180")

    label1 = tk.Label(show_progress, text="Patience!", justify="center")
    progress_bar = ttk.Progressbar(show_progress, length=200, mode="determinate")
    label1.pack(pady=(30, 1))
    progress_bar.pack(pady=(1, 2))
    show_progress.after(0, start_trans) # after方法 第二个参数之后是收集参数

class V2sTrans:
    def __init__(self, root):
      self.root = root
      self.root.geometry(f"500x400+{self.root.winfo_screenwidth()//2-250}+{self.root.winfo_screenheight()//2-200}")
      self.init_win()

    def init_win(self):

      def set_dir():

            path_ = fdl.askopenfilename(title="选择视频文件", filetypes=[("MP4", '.mp4')])

            dir_entry.delete(0, "end")
            dir_entry.insert(0,path_)

      def set_dir_play():
            path_ = fdl.askopenfilename(title="选择v2s文件", filetypes=[("V2S", '.v2s')])

            dir_entry_.delete(0, "end")
            dir_entry_.insert(0, path_)


      l_frame0 = tk.LabelFrame(master=self.root, text="V2S", height=30)
      l_frame0.pack(fill='x', padx=20, pady=10, ipady=50)

      l_frame1 = tk.LabelFrame(master=l_frame0, text="输入mp4文件路径")
      l_frame1.pack(fill="x", padx=20, pady=(50, 0))

      dir_entry = tk.Entry(master=l_frame1, width=40)
      dir_choice_button = tk.Button(master=l_frame1, text="...", relief="raised", command=set_dir, width=4)
      dir_entry.grid(row=0, column=0, padx=(45, 5), pady=(5,10))
      dir_choice_button.grid(row=0, column=1, padx=(5, 10), pady=(5,10))

      start_trans_button = tk.Button(master=l_frame0, text="转换", width=20,
                                       command=lambda: video2strs(dir_entry.get(), self.root))
      start_trans_button.pack(pady=10)

      l_frame2 = tk.LabelFrame(master=l_frame0, text="输入v2s文件路径")
      l_frame2.pack(fill="x", padx=20, pady=(20, 0))

      dir_entry_ = tk.Entry(master=l_frame2, width=40)
      dir_choice_button_ = tk.Button(master=l_frame2, text="...", relief="raised", command=set_dir_play, width=4)
      dir_entry_.grid(row=0, column=0, padx=(45, 5), pady=(5,10))
      dir_choice_button_.grid(row=0, column=1, padx=(5, 10), pady=(5,10))

      start_trans_button = tk.Button(master=l_frame0, text="播放", width=20,
                                       command=lambda: play_v2s(dir_entry_.get()))

      start_trans_button.pack(pady=10)

def play_v2s(path_):
    with open(path_, "r", encoding="utf-8") as f:
      v2s = f.readlines()
      height = int(v2s.split("\\"))
      print(height)
      v2s.pop(0)

    frames = []

    count = 0
    temp_str = ""
    for i in v2s:
      if count == height:
            count = 0
            frames.append(temp_str)
            temp_str = ""
      temp_str += i
      count += 1

    frames_iter = iter(frames)

    def update_frame():
      label.config(text=next(frames_iter))
      label.pack()
      show_v2s.after(27, update_frame)


    show_v2s = tk.Tk()
    show_v2s.title("Playing")
    #show_v2s.geometry(f"800x600+{show_v2s.winfo_screenwidth()//2-400}+{show_v2s.winfo_screenheight()//2-300}")
    label = tk.Label(show_v2s, text="Ready", justify="center", width=1024, height=720, font=("微软雅黑", 6))
    label.pack()
    show_v2s.after(3000, update_frame)
    show_v2s.mainloop()

if __name__ == '__main__':
    root_ = tk.Tk()
    root__ = V2sTrans(root_)
    root__.root.mainloop()

FishC 发表于 2026-4-5 02:49:16

首先,我来帮你修正这段视频转字符画的代码。主要问题包括语法错误、逻辑错误和未定义的变量。以下是修正后的代码,并附上关键说明。

关键修正点:

1. 文件扩展名检查:使用 path_.endswith('.mp4') 替代错误的 split 比较。
2. 文件路径拼接:正确获取文件名(不带扩展名)来生成输出文件路径。
3. 字符映射:定义了 ascii_chars 字符串,用于将灰度值映射到字符。
4. 帧处理:在转换每个帧时,正确遍历像素并映射到字符。
5. 进度条更新:使用 progress_bar['value'] 来更新进度。
6. v2s 文件解析:在播放部分,正确读取和分割帧数据。

修正后的代码:


import cv2
import tkinter as tk
import os.path as op
from tkinter import messagebox as mgb
from tkinter import ttk
from tkinter import filedialog as fdl

def video2strs(path_, root):
    if not op.isfile(path_) or not path_.endswith('.mp4'):
      mgb.showwarning(title="Warning", message="目前仅支持MP4格式的转换!!")
      return

    dir_name, file_name = op.split(path_)
    file_base_name = op.splitext(file_name)# 获取不带扩展名的文件名
    new_text_position = op.join(dir_name, file_base_name)# 使用 os.path.join 确保路径正确

    _ = 0
    suffix = ".v2s"
    while op.isfile(new_text_position + suffix):
      _ += 1
      suffix = f"({_}).v2s"
    new_text_position += suffix
    print("输出文件路径:", new_text_position)

    def get_resize_height():
      cap = cv2.VideoCapture(path_)
      ret, temp_frame = cap.read()
      if ret:
            # 调整帧大小,这里设置为宽度120,高度按比例计算
            height, width = temp_frame.shape[:2]
            new_width = 120
            new_height = int(height * (new_width / width) * 0.5)# 高度减半以匹配字符画比例
            temp_frame = cv2.resize(temp_frame, (new_width, new_height))
            cap.release()
            return (new_height, new_width)# 返回调整后的高度和宽度
      cap.release()
      return (0, 0)

    def start_trans():
      # 定义字符映射,用于将灰度值转换为字符
      ascii_chars = "@%#*+=-:. "

      text_f = open(new_text_position, "w", encoding="utf-8")
      resize_dim = get_resize_height()
      text_f.write(f"{resize_dim},{resize_dim}\n")# 写入高度和宽度,用逗号分隔

      video = cv2.VideoCapture(path_)
      amount_of_frame = int(video.get(cv2.CAP_PROP_FRAME_COUNT))
      one_of_ten = max(amount_of_frame // 10, 1)# 避免除零
      count = 0

      def trans():
            nonlocal count
            if video.isOpened():
                ret, current_frame = video.read()
                if ret:
                  # 调整帧大小并转换为灰度图
                  current_frame = cv2.cvtColor(current_frame, cv2.COLOR_BGR2GRAY)
                  current_frame = cv2.resize(current_frame, (resize_dim, resize_dim))
                  temp_text = ""
                  for i in range(resize_dim):# 高度
                        for j in range(resize_dim):# 宽度
                            gray = current_frame
                            # 将灰度值映射到字符索引
                            index = int(gray / 255 * (len(ascii_chars) - 1))
                            temp_text += ascii_chars
                        temp_text += '\n'
                  text_f.write(temp_text)
                  count += 1
                  # 更新进度条
                  if count % one_of_ten == 0:
                        progress_bar['value'] += 10
                        progress_bar.update()
                  show_progress.after(1, trans)# 使用 after 继续处理下一帧
                else:
                  # 处理完成
                  label1.config(text="OK!")
                  progress_bar['value'] = 100
                  progress_bar.update()
                  text_f.close()
                  video.release()
                  tk.Button(show_progress, text="Quit", command=show_progress.destroy, width=5).pack(pady=15)
            else:
                mgb.showerror("Error", "无法打开视频文件")

      show_progress.after(0, trans)

    # 创建进度窗口
    show_progress = tk.Toplevel(root)# 使用 Toplevel 而非 Tk,避免多个主窗口
    show_progress.geometry("300x180")
    show_progress.title("转换进度")

    label1 = tk.Label(show_progress, text="Patience!", justify="center")
    progress_bar = ttk.Progressbar(show_progress, length=200, mode="determinate", maximum=100)
    label1.pack(pady=(30, 1))
    progress_bar.pack(pady=(1, 2))
    show_progress.after(0, start_trans)

class V2sTrans:
    def __init__(self, root):
      self.root = root
      self.root.geometry(f"500x400+{self.root.winfo_screenwidth()//2-250}+{self.root.winfo_screenheight()//2-200}")
      self.root.title("视频转字符画")
      self.init_win()

    def init_win(self):
      def set_dir():
            path_ = fdl.askopenfilename(title="选择视频文件", filetypes=[("MP4 files", "*.mp4")])
            if path_:
                dir_entry.delete(0, "end")
                dir_entry.insert(0, path_)

      def set_dir_play():
            path_ = fdl.askopenfilename(title="选择v2s文件", filetypes=[("V2S files", "*.v2s")])
            if path_:
                dir_entry_.delete(0, "end")
                dir_entry_.insert(0, path_)

      l_frame0 = tk.LabelFrame(master=self.root, text="V2S", height=30)
      l_frame0.pack(fill='x', padx=20, pady=10, ipady=50)

      l_frame1 = tk.LabelFrame(master=l_frame0, text="输入mp4文件路径")
      l_frame1.pack(fill="x", padx=20, pady=(50, 0))

      dir_entry = tk.Entry(master=l_frame1, width=40)
      dir_choice_button = tk.Button(master=l_frame1, text="...", relief="raised", command=set_dir, width=4)
      dir_entry.grid(row=0, column=0, padx=(45, 5), pady=(5,10))
      dir_choice_button.grid(row=0, column=1, padx=(5, 10), pady=(5,10))

      start_trans_button = tk.Button(master=l_frame0, text="转换", width=20,
                                       command=lambda: video2strs(dir_entry.get(), self.root))
      start_trans_button.pack(pady=10)

      l_frame2 = tk.LabelFrame(master=l_frame0, text="输入v2s文件路径")
      l_frame2.pack(fill="x", padx=20, pady=(20, 0))

      dir_entry_ = tk.Entry(master=l_frame2, width=40)
      dir_choice_button_ = tk.Button(master=l_frame2, text="...", relief="raised", command=set_dir_play, width=4)
      dir_entry_.grid(row=0, column=0, padx=(45, 5), pady=(5,10))
      dir_choice_button_.grid(row=0, column=1, padx=(5, 10), pady=(5,10))

      start_play_button = tk.Button(master=l_frame0, text="播放", width=20,
                                    command=lambda: play_v2s(dir_entry_.get()))
      start_play_button.pack(pady=10)

def play_v2s(path_):
    if not op.isfile(path_) or not path_.endswith('.v2s'):
      mgb.showwarning(title="Warning", message="请选择有效的 .v2s 文件!")
      return

    with open(path_, "r", encoding="utf-8") as f:
      lines = f.readlines()
      if not lines:
            mgb.showerror("Error", "文件为空或格式错误")
            return
      # 第一行是高度和宽度
      first_line = lines.strip()
      try:
            height, width = map(int, first_line.split(','))
      except:
            mgb.showerror("Error", "文件格式错误:第一行应为高度,宽度")
            return
      # 移除第一行
      char_lines = lines

    # 将字符数据分割成帧
    frames = []
    temp_str = ""
    count = 0
    for line in char_lines:
      if count < height:
            temp_str += line
            count += 1
      if count == height:
            frames.append(temp_str)
            temp_str = ""
            count = 0
    # 检查是否有剩余数据
    if temp_str:
      frames.append(temp_str)

    if not frames:
      mgb.showerror("Error", "没有有效的帧数据")
      return

    frames_iter = iter(frames)
    show_v2s = tk.Tk()
    show_v2s.title("Playing")
    # 设置窗口大小,根据字符画尺寸调整
    label = tk.Label(show_v2s, text="Ready", justify="left", font=("Courier", 10))# 使用等宽字体
    label.pack()

    def update_frame():
      try:
            frame_text = next(frames_iter)
            label.config(text=frame_text)
            show_v2s.after(27, update_frame)# 约37帧每秒,但字符画可能较慢,可调整
      except StopIteration:
            label.config(text="播放完毕")
            tk.Button(show_v2s, text="关闭", command=show_v2s.destroy).pack()

    show_v2s.after(100, update_frame)# 延迟开始播放
    show_v2s.mainloop()

if __name__ == '__main__':
    root_ = tk.Tk()
    app = V2sTrans(root_)
    root_.mainloop()


注意事项:

1. 字符映射:使用了 ascii_chars = "@%#*+=-:. " 作为灰度到字符的映射,你可以根据需要调整字符顺序或数量。
2. 帧大小调整:在 get_resize_height 中,将帧宽度固定为120,高度按比例计算并减半,以适应字符画的比例(字符通常高度大于宽度)。
3. 进度条更新:使用 progress_bar['value'] += 10 来逐步更新进度。
4. 播放部分:使用 font=("Courier", 10) 等宽字体,以确保字符对齐。
5. 错误处理:添加了基本的错误检查,如文件是否存在、格式是否正确。

运行前请确保已安装 OpenCV:可以通过 pip install opencv-python 安装。如果遇到问题,请检查视频文件路径和输出目录的写入权限。

以上回复来自 FishC AI 小助理,如未能正确解答您的问题,请继续追问。

不二如是 发表于 2026-4-5 14:32:13

加一些效果演示的图片或动图会更好
页: [1]
查看完整版本: 视频转字符画