Python 视频转字符画 - 进阶

2023-05-16

昨晚我在网上看到了别人做的视频转字符动画,觉得很厉害,我于是也打算玩玩。今天中午花时间实现了这样一个小玩意。
顺便把过程记录在这里。

        1.源视频:https://docs.qq.com/doc/DU3pabWFhWGpXWW5h

        2.转换后:

步骤

1、将视频转化为一帧一帧的图片

 2、把图片转化为字符画

 3、按顺序播放字符画

一、准备

1、模块

这个程序需要用到这样几个模块:

1. opencv-python #用来读取视频和图片
2. numpy # opencv-python依赖于它

准备阶段,首先安装依赖: .

pip3 install numpy opencv-python

然后新建python代码文档,在开头添加上下面的导入语句

#-*- coding:utf-8 -*-

# numpy 是一个矩阵运算库,图像处理需要用到。
import numpy as np

 2、材料

材料就是需要转换的视频文件了,下载下来和代码放到同一目录下,你也可以换成自己的,建议是学习时尽量选个短一点的视频,几十秒就行了,不然调试起来很痛苦。(或者自己稍微修改一下函数,只转换一定范围、一定数量的帧。)此外,要选择对比度高的视频。否则的话,就需要彩色字符才能有足够好的表现,有时间我试试。

二、按帧读取视频

现在继续添加代码,实现第一步:按帧读取视频。
下面这个函数,接受视频路径和字符视频的尺寸信息,返回一个img列表,其中的img是尺寸都为指定大小的灰度图。

#导入 opencv
import cv2

def video2imgs(video_name, size):
    """

    :param video_name: 字符串, 视频文件的路径
    :param size: 二元组,(宽, 高),用于指定生成的字符画的尺寸
    :return: 一个 img 对象的列表,img对象实际上就是 numpy.ndarray 数组
    """

    img_list = []

    # 从指定文件创建一个VideoCapture对象
    cap = cv2.VideoCapture(video_name)

    # 如果cap对象已经初始化完成了,就返回true,换句话说这是一个 while true 循环
    while cap.isOpened():
        # cap.read() 返回值介绍:
        #   ret 表示是否读取到图像
        #   frame 为图像矩阵,类型为 numpy.ndarry.
        ret, frame = cap.read()
        if ret:
            # 转换成灰度图,也可不做这一步,转换成彩色字符视频。
            gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

            # resize 图片,保证图片转换成字符画后,能完整地在命令行中显示。
            img = cv2.resize(gray, size, interpolation=cv2.INTER_AREA)

            # 分帧保存转换结果
            img_list.append(img)
        else:
            break

    # 结束时要释放空间
    cap.release()

    return img_list

写完后可以写个main方法测试一下,像这样:

if __name__ == "__main__":
    imgs = video2imgs("BadApple.mp4", (64, 48))
    assert len(imgs) > 10

如果运行没报错,就没问题
代码里的注释应该写得很清晰了,继续下一步。

三、图像转化为字符画

视频转换成了图像,这一步便是把图像转换成字符画
下面这个函数,接受一个img对象为参数,返回对应的字符画。

# 用于生成字符画的像素,越往后视觉上越明显。。这是我自己按感觉排的,你可以随意调整。
pixels = " .,-'`:!1+*abcdefghijklmnopqrstuvwxyz<>()\/{}[]?234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ%&@#$"

def img2chars(img):
    """

    :param img: numpy.ndarray, 图像矩阵
    :return: 字符串的列表:图像对应的字符画,其每一行对应图像的一行像素
    """
    res = []

    # 灰度是用8位表示的,最大值为255。
    # 这里将灰度转换到0-1之间
    # 使用 numpy 的逐元素除法加速,这里 numpy 会直接对 img 中的所有元素都除以 255
    percents = img / 255

    # 将灰度值进一步转换到 0 到 (len(pixels) - 1) 之间,这样就和 pixels 里的字符对应起来了
    # 同样使用 numpy 的逐元素算法,然后使用 astype 将元素全部转换成 int 值。
    indexes = (percents * (len(pixels) - 1)).astype(np.int) 
    
    # 要注意这里的顺序和 之前的 size 刚好相反(numpy 的 shape 返回 (行数、列数))
    height, width = img.shape
    for row in range(height):
        line = ""
        for col in range(width):

            index = indexes[row][col]
            # 添加字符像素(最后面加一个空格,是因为命令行有行距却没几乎有字符间距,用空格当间距)
            line += pixels[index] + " "
        res.append(line)

    return res

上面的函数只接受一帧为参数,一次只转换一帧,可我们需要的是转换所有的帧,所以就再把它包装一下:

def imgs2chars(imgs):
    video_chars = []
    for img in imgs:
        video_chars.append(img2chars(img))

    return video_chars

好了,现在我们可以测试一下:

if __name__ == "__main__":
    imgs = video2imgs("BadApple.mp4", (64, 48))
    video_chars = imgs2chars(imgs)
    assert len(video_chars) > 10

没报错的话,就可以下一步了。(这一步比较慢,测试阶段建议用短一点的视频,或者稍微改一下,只处理前30秒之类的)

四、播放字符视频

写了这么多代码,现在终于要出成果了。现在就是最激动人心的一步:播放字符画了。同样的,我把它封装成了一个函数。下面这个函数接受一个字符画的列表并播放。

通用版(使用 shell 的 clear 命令清屏,但是因为效率不高,可能会有一闪一闪的问题)这个版本适用于 linux/windows

# 导入需要的模块
import time
import subprocess

def play_video(video_chars):
    """
    播放字符视频
    :param video_chars: 字符画的列表,每个元素为一帧
    :return: None
    """
    # 获取字符画的尺寸
    width, height = len(video_chars[0][0]), len(video_chars[0])

    for pic_i in range(len(video_chars)):
        # 显示 pic_i,即第i帧字符画
        for line_i in range(height):
            # 将pic_i的第i行写入第i列。
            print(video_chars[pic_i][line_i])
        time.sleep(1 / 24)  # 粗略地控制播放速度。

        # 调用 shell 命令清屏
        subprocess.run("clear", shell=True)  # linux 版
        # subrpocess.run("cls", shell=True)  # cmd 版,windows 系统请用这一行。

好,接下来就是见证奇迹的时刻
不过开始前要注意,字符画的播放必须在shell窗口下运行,在pycharm里运行会看到一堆无意义字符。另外播放前要先最大化shell窗口

if __name__ == "__main__":
    imgs = video2imgs("BadApple.mp4", (64, 48))
    video_chars = imgs2chars(imgs)
    input("`转换完成!按enter键开始播放")
    play_video(video_chars)

写完后,开个shell,最大化窗口,然后键入(文件名换成你的)

python3 video2chars.py

可能要等很久。我使用示例视频大概需要 12 秒左右。看到提示的时候,按回车,开始播放!

********这样就完成了视频到字符动画的转换, 除去注释, 大概七十行代码的样子. 稍微超出了点预期, 不过效果真是挺棒的

五、速度优化

要是每次播放都要等个一分钟,也太痛苦了一点。

所以可以用 pickle 模块把 video_chars 保存下来,下次播放时,如果发现当前目录下有这个保存下来的数据,就跳过转换,直接播放了。这样就快多了。

只需要改一下测试代码,

先在开头添加两个依赖


import os
import pickle

然后在文件结尾添加代码:  

def dump(obj, file_name):
    """
    将指定对象,以file_nam为名,保存到本地
    """
    with open(file_name, 'wb') as f:
        pickle.dump(obj, f)
    return


def load(filename):
    """
    从当前文件夹的指定文件中load对象
    """
    with open(filename, 'rb') as f:
        return pickle.load(f)


def get_file_name(file_path):
    """
    从文件路径中提取出不带拓展名的文件名
    """
    # 从文件路径获取文件名 _name
    path, file_name_with_extesion = os.path.split(file_path)

    # 拿到文件名前缀
    file_name, file_extern = os.path.splitext(file_name_with_extesion)

    return file_name


def has_file(path, file_name):
    """
    判断指定目录下,是否存在某文件
    """
    return file_name in os.listdir(path)


def get_video_chars(video_path, size):
    """
    返回视频对应的字符视频
    """
    video_dump = get_file_name(video_path) + ".pickle"

    # 如果 video_dump 已经存在于当前文件夹,就可以直接读取进来了
    if has_file(".", video_dump):
        print("发现该视频的转换缓存,直接读取")
        video_chars = load(video_dump)
    else:
        print("未发现缓存,开始字符视频转换")

        print("开始逐帧读取")
        # 视频转字符动画
        imgs = video2imgs(video_path, size)

        print("视频已全部转换到图像, 开始逐帧转换为字符画")
        video_chars = imgs2chars(imgs)

        print("转换完成,开始缓存结果")
        # 把转换结果保存下来
        dump(video_chars, video_dump)
        print("缓存完毕")

    return video_chars


if __name__ == "__main__":
    # 宽,高
    size = (64, 48)
    # 视频路径,换成你自己的
    video_path = "BadApple.mp4"
    video_chars = get_video_chars(video_path, size)
    play_video(video_chars)  

另一个优化方法就是边转换边播放,就是同时执行上述三个步骤。学会了的话,可以自己实现一下试试。

4. 字符视频和音乐同时播放

没有配乐的动画,虽然做出来了是很有成就感,但是你可能看上两遍就厌倦了。

所以让我们来给它加上配乐。(不要担心,其实就只需要添加几行代码而已),首先我们需要找个方法来播放视频的配乐,怎么做呢?先介绍一下一个跨平台视频播放器:mpv,它有很棒的命令行支持,请先安装好它。

要让 mpv 只播放视频的音乐部分,只需要命令:


mpv --no-video video_path  

好了,现在有了音乐,可总不能还让人开俩shell,先放音乐,再放字符画吧。

这时候,我们需要的功能是:使用python调用外部应用

但是 mpv 使用了类似 curses 的功能,标准库的 os.system 和 subprocess 都不能隐藏掉这个部分,播放效果不尽如人意。

因此我使用了 pyinvoke 模块,只要给它指定参数 hide=True ,就可以完美隐藏掉被调用程序的输出。

好了废话说这么多,上代码:


import invoke
    video_path = "BadApple.mp4"
invoke.run(f"mpv --no-video {video_path}", hide=True, warn=True)  

运行上面的测试代码,如果听到了音乐,而shell啥都没输出的话,就正常了。我们继续。

音乐已经有了,那就好办了。

添加一个播放音乐的函数


import invoke
def play_audio(video_path):
    invoke.run(f"mpv --no-video {video_path}", hide=True, warn=True)  

然后修改main()方法:


def main():
    # 宽,高
    size = (64, 48)
    # 视频路径,换成你自己的
    video_path = "BadApple.mp4"

    # 只转换三十秒,这个属性是才添加的,但是上一篇的代码没有更新。你可能需要先上github看看最新的代码。其实就稍微改了一点。
    seconds = 30
    
    # 这里的fps是帧率,也就是每秒钟播放的的字符画数。用于和音乐同步。这个更新也没写进上一篇,请上github看看新代码。
    video_chars, fps = get_video_chars(video_path, size, seconds)

    # 播放音轨
    play_audio(video_path)

    # 播放视频
    play_video(video_chars, fps)


if __name__ == "__main__":
    main()  

然后运行。。并不是我坑你,你只听到了声音,却没看到字符画。。原因是: invoke.run()函数是阻塞的,音乐没放完,代码就到不了 play_video(video_chars, fps) 这一行。

所以 play_audio 还要改一下,改成这样:


import invoke
from threading import Thread

def play_audio(video_path):
    def call():
        invoke.run(f"mpv --no-video {video_path}", hide=True, warn=True)

    # 这里创建子线程来执行音乐播放指令,因为 invoke.run() 是一个阻塞的方法,要同时播放字符画和音乐的话,就要用多线程/进程。
    p = Thread(target=call)
    p.setDaemon(True)
    p.start()  

这里使用标准库的 threading.Thread 类来创建子线程,让音乐的播放在子线程里执行,然后字符动画还是主线程执行,Ok,这就可以看到最终效果了。实际上只添加了十多行代码而已。

六、总结

完整代码见 https://docs.qq.com/doc/DU3pabWFhWGpXWW5h,要注意的是代码库的代码,包含了(音频、缓存、帧率控制等),而且相对文章也有一些小改动(目的是方便使用,但是稍微增加了点代码量),动手操作起来吧。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Python 视频转字符画 - 进阶 的相关文章

随机推荐

  • CCF CSP 序列查询新解

    CCF CSP 序列查询新解 C语言 题目背景 上一题 序列查询 中说道 xff1a A 61 A0 A1 A2 An 是一个由 n 43 1 个 0 N 范围内整数组成的序列 xff0c 满足 0 61 A0 lt A1 lt A2 lt
  • 打开edge,出现错误代码: STATUS_ACCESS_DENIED,解决方法

    问题描述 xff1a 这个问题 xff0c 一般显示edge页面加载失败 xff0c 且电脑右下角出现弹窗 解决办法 xff1a 此时 xff0c 你可以到C盘 xff0c 把Microsoft下的子文件删掉 xff0c 注意最好用dele
  • JS的表达式

    一 原始表达式 1 表达式是js的代码形式 2 原始表达式包含直接量 关键字和变量名 xff1b lt script gt 直接量 100 xff1b 200 2 xff1b 34 hi 34 关键字 True False 变量名 a 函数
  • JS的DOM操作——style的操作

    对于JS操作文档中的元素 xff0c 改变其的样式特征需要用到一个属性 style 常见操作 xff1a 获取的元素点 xff08 xff09 style xx 需要设置或修改的属性 代码演示 xff1a 例如修改div块的背景颜色 lt
  • 学会iframe并用其解决跨域问题

    了解iframe 官方定义为 xff1a iframe是HTML标签 xff0c 作用是文档中的文档 xff0c 或者浮动的框架 FRAME iframe元素会创建包含另外一个文档的内联框架 xff08 即行内框架 xff09 简单理解为
  • Vue中的样式绑定

    我们将样式绑定分为两种 xff1a 一种是通过style绑定 xff0c 一种是通过class绑定 样式绑定适用于 切换效果的实现 xff0c 小规模用style xff0c 大规模用class style绑定样式 这里需要用到v bind
  • 51 汇编语言编程:8个按键控制8个LED

    89C51汇编语言写8个按键控制8个LED 按键接P2口 xff0c LED接P1口 有6按键 xff0c 按下对应LED长亮 xff0c 重复按下 xff0c LED灭 xff1b 有两个特殊按键 xff0c 按下 xff0c 对应LED
  • 常见的排序算法

    今天来学习一下数据结构课程中非常重要的排序算法 我们学习一下一些常见的排序算法 xff0c 如冒泡排序 选择排序 快速排序等 1 冒泡排序 实现思路 xff1a 对未排序的各元素从头到尾依次比较相邻的两个元素大小关系 如果左边的队员大 则两
  • Vue的仓库vuex

    Vue js是一个渐进式的框架 xff0c 是一个分层的设计模式 一共分为五层 xff0c 核心库为基础 xff0c 在这基础上添加组件系统 客户端路由 大规模状态管理和开发环境 其中大数据状态管理对应的技术就是vuex xff0c 它是由
  • vue3.0的setup函数以及解决其内的数据不是响应式数据的问题

    相比于2 0 xff0c vue3 0在新增了一个setup函数 xff0c 我们在setup中可以写数据也可以写方法 xff0c 就像我们以前最开始学习js一样 xff0c 在js文件中写代码 如 xff1a setup let name
  • setup中使用watch

    watch属性监听器的作用在vue3 0中没有改变 xff0c 还是监听值得变化 在vue3 0中 xff0c 仍然支持watch配置项 但是我们要在setup中使用watch得话 xff0c 我们要导入watch的API xff0c 然后
  • 缓存相关知识点

    缓存的优点 1 减少不必要的数据传输 xff0c 节省带宽 2 减轻服务器负担 xff0c 提升网站性能 3 加快客户端网页加载速度 4 用户体验更好 缺点 如果资源发生更改 xff0c 客户端不及时更新会导致用户获取信息滞后 缓存类型 分
  • hook之useMemo

    useMemo 这个hook的作用有点像我们以前学习的计算属性一样 xff0c 它会缓存我们上次的结果 xff0c 只有当特定的属性改变时才从新计算 并且他能 记住 任何类型的值 标准写法 useMemo 61 gt return 34 我
  • hook之useContext

    useContext这个hook xff0c 我们通过翻译就可以大致知道它的用途 xff0c 译为使用上下文 context上下文我们在前面就已经使用过了 xff0c 它是一种用于跨组件之间的传值的技术 我们学过Context xff0c
  • hook之useRoducer

    useReducer用最简单的话来说 xff0c 就是允许我们在函数组件里面像使用redux一样通过reducer和action来管理我们组件状态的变换 我们可以通过useContext配合实现一个函数组件中的redux useReduce
  • linux安装jenkins 【详细教程】

    1 确保jdk已经安装 xff0c 至少是java8以上 xff0c 如果没有安装 xff0c 可以参考https blog csdn net m0 59347746 article details 125681582 csdn share
  • 【笔记】SQL 2

    1 比较运算符 比较运算符用于比较运算 xff0c 判断逻辑是否成立 A operator B 常用的比较运算符有 61 xff08 等于 xff09 61 xff08 不等于 xff09 lt gt xff08 不等于 xff09 lt
  • Qt Creator 3.0.1 部署远程调试

    QT Creator3 0 1 部署远程调试 远程目标板连接配置 从菜单栏选择options gt devices gt add gt generic linux device gt 开始向导 输入目标板的IP与登录用户信息 完成后会自动测
  • 代码源oj--数据结构初级:105 字符串处理1

    给定一个长度为n的字符串s xff0c 字符串由小写字母a z组成 小明来对这个字符串进行操作 xff0c 他会从头到尾检查这个字符串 xff0c 如果发现有两个相同的字母并排在一起 xff0c 就会把这两个字符都删掉 小明会重复这个操作
  • Python 视频转字符画 - 进阶

    昨晚我在网上看到了别人做的视频转字符动画 xff0c 觉得很厉害 xff0c 我于是也打算玩玩 今天中午花时间实现了这样一个小玩意 顺便把过程记录在这里 1 源视频 xff1a https docs qq com doc DU3pabWFh