趣味中秋,用动画字符来贺岁佳节

引言

本篇博文写作初衷是看到中秋活动,正好之前有总结过两篇关于图片与视频转字符动画的博文,以下大部分代码与小部分说明引用自之前写过的笔记,一篇是来自2019年过年的时候写得Python利用argparse模块图片转字符文件 ,以及2020年中旬总结的Python3 & OpenCV 视频转字符动画笔记,不曾想现在都时隔2年多了,那时候还是初学python,因为我是转行的,大学学的是电气,从自学python到现在,一路还是比较唏嘘的。那么话不多说,在此进入正题。

环境准备

在绘制字符前,我们首先得准备好python的相关环境:

pip install pyprind opencv-python pillow argparse
"""
Requirement already satisfied: pyprind in /home/anaconda3/envs/py37_kt/lib/python3.6/site-packages (2.11.3)
Requirement already satisfied: opencv-python in /home/anaconda3/envs/py37_kt/lib/python3.6/site-packages (4.5.2.54)
Requirement already satisfied: numpy>=1.13.3 in /home/anaconda3/envs/py37_kt/lib/python3.6/site-packages (from opencv-python) (1.18.5)
"""

确认没有问题后,就能先导入环境所需要的包,这边建议是在linux或者Mac下进行操作,因为我之前测试代码都是在linux上,并且下面有些包在win和linux下还是有区别的,具体如下:

import sys
import os
import time
import threading
"""
termios和tty库只存在于Linux服务器中的python库,win下相同功能的库为msvcrt库。
"""
import termios
import tty
import cv2
import pyprind
from PIL import Image, ImageFont, ImageDraw
import argparse

这里需要说明的是,termios该模块提供了一个用于tty I / O控制的POSIX调用的接口,该库有检测键盘输入,等待按键超时检测等功能。这里主要方便于后续写txt,修改终端的属性为non-canonical模式,而win下其实更简单一些,python默认编译了msvcrtmsvcrt库里有一个getch函数直接就可以搞定。理论上不需要这两个库也行,但我没有具体实践了。

pyprind库与argparse库可以看引言中引出的两篇笔记,前者我做了动图演示效果,后者我画了函数包的思维导图解析,至于opencv的功能,本篇并不对其深入,但会在使用过程中尽心解释,我也有做一个opencv专题,写了11篇开发笔记,可以参考一下。那么到此,就进入主题。

图片转换成字符

字符画是一系列字符的组合,我们可以把字符看作是比较大块的像素,一个字符能表现一种颜色(为了简化可以这么理解),字符的种类越多,可以表现的颜色也越多,图片也会更有层次感。而我们也一般用256的灰度值来表示上述关系,这里抛开精确不讲,大致用一个简单的式子表示为:

g r a y = 0.2126 ∗ r + 0.7152 ∗ g + 0.0722 ∗ b gray = 0.2126 * r + 0.7152 * g + 0.0722 * b gray0.2126r+0.7152g+0.0722b

以此,我们可以创建一个不重复的字符列表ascii_char,灰度值小(暗)的用列表开头的符号,灰度值大(亮)的用列表末尾的符号。这就相当于一种映射,将灰度的区间转换成字符的区间,用字符替代灰度值,那么设当前字符为x,公式为:

g r a y 256.0 = x a s c i i − c h a r ⇒ x = g r a y 256 ∗ a s c i i − c h a r \frac{gray}{256.0} = \frac{x}{ascii-char} \Rightarrow x = \frac{gray}{256}*ascii-char 256.0gray=asciicharxx=256grayasciichar

如果是256个字符,与256的灰度值就是一一对应的关系,所得出来的结果也越精确,字符画也越清晰,但要想出256个不重复的字符,不容易。那么可以写出第一版代码。

我们首先使用嫦娥奔月图作为本篇的第一张图片:
在这里插入图片描述

而我们根据上述理论写出的转字符代码为:

ascii_char = list("$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,\"^`'. ")

# 将256灰度值映射到70个字符上
def get_char(r,g,b,alpha = 256):
    """
    :param r: red
    :param g: green
    :param b: blue
    :param alpha: 图像的灰度值或者说像素值 
    :return: 返回经过公式处理后的70个字符值中对应的区间
    """
    if alpha == 0:  # 可能会出现0的区间,这部分如果不处理会产生报错
        return " "
    gray = 0.2126 * r + 0.7152 * g + 0.0722 * b  # RGB三原色的计算公式
    # print(gray,alpha)  发现gray会超过alpha边界,所以要加1.0,浮点数
    x = int((gray / (alpha + 1.0)) * len(ascii_char))  # 映射转换
    return ascii_char[x]    # 返回我们上面取到的信道区间值

# 写入函数
def write_file(content):
    with open("out_file_name.png","w") as f:
        f.write(content)
# 主函数
def main(file_name="test.jpg",width=80,height=80):
    """
    :param file_name: 指定输入图片路径
    :param width: 宽度,默认80
    :param height: 高度,默认80
    :return: 无返回值
    """
    text = ""
    im = Image.open(file_name)
    im = im.resize((width,height),Image.NEAREST)  # resize对图片进行压缩,NEAREST为KNN插值尺寸
    for i in range(height):
        for j in range(width):
            content = im.getpixel((j,i))  # 宽度代表横坐标,高度代表纵坐标,得到一个元组
            text += get_char(*content)  # 对上面的元组进行解包
        text += "\n"  # 行加上换行符
    # print(text)    # 在终端输出字符
    write_file(text)

if __name__ == '__main__':
    main(file_name="1.jpg")

然后运行代码,结果为:
在这里插入图片描述
完全看不出是什么东西,因为我们只是对于颜色编码空间进行了ASCII转换,但其实很多图片的颜色通道并不是这样的,所以基于此进行改进为:

#-*- coding:utf-8 -*-
import os
from PIL import Image, ImageFont, ImageDraw
import argparse
#命令行输入参数处理
parser = argparse.ArgumentParser()
parser.add_argument('file')
parser.add_argument('-o','--output')


#获取参数
args = parser.parse_args()
File = args.file
OUTPUT = args.output

ascii_char = list("MNHQ$OC67)oa+>!:+. ")

#将像素转换为ascii码
def get_char(r,g,b,alpha = 256):
    if alpha == 0:
        return ''
    length = len(ascii_char)
    gray = int(0.2126 * r + 0.7152 * g + 0.0722 * b)
    unit = (256.0+1)/length
    return ascii_char[int(gray/unit)]

if __name__=='__main__':
    im = Image.open(File)
    WIDTH = int(im.width/6) #高度比例为原图的1/6较好,由于字体宽度
    HEIGHT = int(im.height/15)  #高度比例为原图的1/15较好,由于字体高度
    im_txt = Image.new("RGB",(im.width,im.height),(255,255,255))
    im = im.resize((WIDTH,HEIGHT),Image.NEAREST)
    txt = ""
    colors = []
    for i in range(HEIGHT):
        for j in range(WIDTH):
            pixel = im.getpixel((j,i))
            colors.append((pixel[0],pixel[1],pixel[2]))#记录像素颜色信息
            if(len(pixel) == 4):
                txt += get_char(pixel[0],pixel[1],pixel[2],pixel[3])
            else:
                txt += get_char(pixel[0],pixel[1],pixel[2])        
        txt += '\n' 
        colors.append((255,255,255))
    dr = ImageDraw.Draw(im_txt)
    font=ImageFont.load_default().font#获取字体
    x=y=0
    #获取字体的宽高
    font_w,font_h=font.getsize(txt[1])
    font_h *= 1.37 #调整后更佳
    #ImageDraw为每个ascii码进行上色
    for i in range(len(txt)):
        if(txt[i]=='\n'):
            x+=font_h
            y=-font_w
        dr.text([y,x],txt[i],colors[i])
        y+=font_w
    #输出
    im_txt.save("output.png")

因为有用到argparse模块,这是一个命令行输入模块,具体的操作方法在上面博客链接中有画出它的思维导图实例,而这里将上面的代码封装进了color_ascii.py文件中,选取了一张彩色图片,调用并保存为:
在这里插入图片描述
这里就能清晰看到,情满中秋字符图清晰显现,是不是有內味了,emmm

视频转字符动画

首先,这里跟上一节的图片逻辑一样,先将转换字符的功能定义成类:

class CharFrame:

    ascii_char = "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,\"^`'. "

    # 像素映射到字符
    def pixelToChar(self, luminance):
        return self.ascii_char[int(luminance/256*len(self.ascii_char))]

    # 将普通帧转为 ASCII 字符帧
    def convert(self, img, limitSize=-1, fill=False, wrap=False):
        if limitSize != -1 and (img.shape[0] > limitSize[1] or img.shape[1] > limitSize[0]):
            img = cv2.resize(img, limitSize, interpolation=cv2.INTER_AREA)
        ascii_frame = ''
        blank = ''
        if fill:
            blank += ' '*(limitSize[0]-img.shape[1])
        if wrap:
            blank += '\n'
        for i in range(img.shape[0]):
            for j in range(img.shape[1]):
                ascii_frame += self.pixelToChar(img[i,j])
            ascii_frame += blank
        return ascii_frame
  • pixelToChar() 方法:只有一个参数,其接收像素的亮度信息。需要注意的是,方法的 return 语句中的表达式使用了一个数值 256,虽然像素的亮度范围是 0~255,但若是把 256 更改为 255,那么这个表达式将有可能引发 IndexError 异常。

  • convert()方法:有一个位置参数三个可选参数,参数 img 接收一个对象,这个对象类型是 numpy.ndarray,也就是 OpenCV 打开图片返回的对象,同样,之后用 OpenCV 得到的视频的帧也是这个对象。limitSize 参数接受一个元组,表示图片的限宽限高。fill 表示是否用空格填充图片宽度至限宽,wrap 表示是否在行末添加换行符。

  • cv2.resize( src, dst, dsize, fx=0, fy=0, interpolation=INTER_LINEAR )

    fx,fy表示的是放缩的比例,interpolation代表差值的类型,其具体的形式主要有以下:

      INTER_NEAREST(邻近元素插值法)

      INTER_LINEAR(缺省值,双线性插值)

      INTER_AREA(使用象素关系重采样。当图像缩小时候,该方法可以避免波纹出现。当图像放大时,类似于 CV_INTER_NN 方法)

      INTER_CUBIC(立方插值)

然后定义我们的主要转义加输出类,继承字符类:

class V2Char(CharFrame):

    charVideo = []
    timeInterval = 0.033

    def __init__(self, path):
        if path.endswith('txt'):
            self.load(path)
        else:
            self.genCharVideo(path)

    def genCharVideo(self, filepath):
        self.charVideo = []
        cap = cv2.VideoCapture(filepath)
        self.timeInterval = round(1/cap.get(5), 3)
        nf = int(cap.get(7))
        print('Generate char video, please wait...')
        for i in pyprind.prog_bar(range(nf)):
            rawFrame = cv2.cvtColor(cap.read()[1], cv2.COLOR_BGR2GRAY)
            frame = self.convert(rawFrame, os.get_terminal_size(), fill=True)
            self.charVideo.append(frame)
        cap.release()

    def export(self, filepath):
        if not self.charVideo:
            return
        with open(filepath,'w') as f:
            for frame in self.charVideo:
                # 加一个换行符用以分隔每一帧
                f.write(frame + '\n')

    def load(self, filepath):
        self.charVideo = []
        # 一行即为一帧
        for i in  open(filepath):
            self.charVideo.append(i[:-1])

    def play(self, stream = 1):
        # Bug:
        # 光标定位转义编码不兼容 Windows
        if not self.charVideo:
            return
        if stream == 1 and os.isatty(sys.stdout.fileno()):
            self.streamOut = sys.stdout.write
            self.streamFlush = sys.stdout.flush
        elif stream == 2 and os.isatty(sys.stderr.fileno()):
            self.streamOut = sys.stderr.write
            self.streamFlush = sys.stderr.flush
        elif hasattr(stream, 'write'):
            self.streamOut = stream.write
            self.streamFlush = stream.flush

        old_settings = None
        breakflag = None
        # 获得标准输入的文件描述符
        fd = sys.stdin.fileno()

        def getChar():
            nonlocal breakflag
            nonlocal old_settings
            # 保存标准输入的属性
            old_settings = termios.tcgetattr(fd)
            # 设置标准输入为原始模式
            tty.setraw(sys.stdin.fileno())
            # 读取一个字符
            ch = sys.stdin.read(1)
            breakflag = True if ch else False

        # 创建线程
        getchar = threading.Thread(target=getChar)
        # 设置为守护线程
        getchar.daemon = True
        # 启动守护线程
        getchar.start()
        # 输出的字符画行数
        rows = len(self.charVideo[0])//os.get_terminal_size()[0]
        for frame in self.charVideo:
            # 接收到输入则退出循环
            if breakflag is True:
                break
            self.streamOut(frame)
            self.streamFlush()
            time.sleep(self.timeInterval)
            # 共 rows 行,光标上移 rows-1 行回到开始处
            self.streamOut('\033[{}A\r'.format(rows-1))
        # 恢复标准输入为原来的属性
        termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
        # 光标下移 rows-1 行到最后一行,清空最后一行
        self.streamOut('\033[{}B\033[K'.format(rows-1))
        # 清空最后一帧的所有行(从倒数第二行起)
        for i in range(rows-1):
            # 光标上移一行
            self.streamOut('\033[1A')
            # 清空光标所在行
            self.streamOut('\r\033[K')
        info = 'User interrupt!\n' if breakflag else 'Finished!\n'
        self.streamOut(info)

这个类有两个主要方法,一个是从视频文件转为字符动画的方法 genCharVideo(),一个是播放字符动画的方法 play()。调用 genCharVideo() 方法将视频转化为字符动画存放到属性 charVideo。

另外由于从视频转到字符动画是一个耗时耗力的过程,所以我们可以把 charVideo 中的字符动画数据导出来,方便下次读取播放,这就意味着要有导出和读取方法,即 export()方法和 load() 方法。

下面是类方法中的函数介绍:

  • cv2.cvtColor() 用来转换图像的颜色空间,第一个参数为图像对象,第二个参数指示转换类型,OpenCV 中有超过 150 个颜色空间转换,这里我们使用的是彩色转灰度 cv2.COLOR_BGR2GRAY。

  • pyprind.prog_bar():是一个生成器,使用这个生成器进行迭代会自然在终端中输出进度条。

  • cap.read():读取视频的下一帧,其返回一个两元素的元组,第一个元素为 bool 值,指示帧是否被正确读取,第二个元素为 numpy.ndarray ,其存放的便是帧的数据。

  • os.get_terminal_size():方法返回当前终端的列数(宽),行数(高)。这里我们将 fill 参数设置为 True,未设置 wrap 参数,其值为默认的 False。在终端里如果打印的字符超过一行的宽度,终端会自动进行显示上的换行。

  • ·play() 方法:接受一个参数,表示使用哪种输出流。

    1.代表输出流使用标准输出 sys.stdout
    2. 代表输出流使用标准错误输出 sys.stderr

    其中 sys.stdout.fileno() 和 sys.stderr.fileno() 分别返回标准输出和标准错误输出的文件描述符。os.isatty(fd) 返回一个布尔值,当文件描述符 fd 是打开并连接到 tty 设备时返回真。

这里的play函数有两个地方需要注意:

  1. 不要使用 input() 方法接收字符输入

  2. 不能使用普通线程

还有play函数里的转义字符对应的功能,具体说明参考引言中引用的参考文献,这里不再多说,所以将这两个类合起来,代码为:


class CharFrame:

    ascii_char = "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,\"^`'. "

    # 像素映射到字符
    def pixelToChar(self, luminance):
        return self.ascii_char[int(luminance/256*len(self.ascii_char))]

    # 将普通帧转为 ASCII 字符帧
    def convert(self, img, limitSize=-1, fill=False, wrap=False):
        if limitSize != -1 and (img.shape[0] > limitSize[1] or img.shape[1] > limitSize[0]):
            img = cv2.resize(img, limitSize, interpolation=cv2.INTER_AREA)
        ascii_frame = ''
        blank = ''
        if fill:
            blank += ' '*(limitSize[0]-img.shape[1])
        if wrap:
            blank += '\n'
        for i in range(img.shape[0]):
            for j in range(img.shape[1]):
                ascii_frame += self.pixelToChar(img[i,j])
            ascii_frame += blank
        return ascii_frame


class V2Char(CharFrame):

    charVideo = []
    timeInterval = 0.033

    def __init__(self, path):
        if path.endswith('txt'):
            self.load(path)
        else:
            self.genCharVideo(path)

    def genCharVideo(self, filepath):
        self.charVideo = []
        cap = cv2.VideoCapture(filepath)
        self.timeInterval = round(1/cap.get(5), 3)
        nf = int(cap.get(7))
        print('Generate char video, please wait...')
        for i in pyprind.prog_bar(range(nf)):
            rawFrame = cv2.cvtColor(cap.read()[1], cv2.COLOR_BGR2GRAY)
            frame = self.convert(rawFrame, os.get_terminal_size(), fill=True)
            self.charVideo.append(frame)
        cap.release()

    def export(self, filepath):
        if not self.charVideo:
            return
        with open(filepath,'w') as f:
            for frame in self.charVideo:
                # 加一个换行符用以分隔每一帧
                f.write(frame + '\n')

    def load(self, filepath):
        self.charVideo = []
        # 一行即为一帧
        for i in  open(filepath):
            self.charVideo.append(i[:-1])

    def play(self, stream = 1):
        # Bug:
        # 光标定位转义编码不兼容 Windows
        if not self.charVideo:
            return
        if stream == 1 and os.isatty(sys.stdout.fileno()):
            self.streamOut = sys.stdout.write
            self.streamFlush = sys.stdout.flush
        elif stream == 2 and os.isatty(sys.stderr.fileno()):
            self.streamOut = sys.stderr.write
            self.streamFlush = sys.stderr.flush
        elif hasattr(stream, 'write'):
            self.streamOut = stream.write
            self.streamFlush = stream.flush

        old_settings = None
        breakflag = None
        # 获得标准输入的文件描述符
        fd = sys.stdin.fileno()

        def getChar():
            nonlocal breakflag
            nonlocal old_settings
            # 保存标准输入的属性
            old_settings = termios.tcgetattr(fd)
            # 设置标准输入为原始模式
            tty.setraw(sys.stdin.fileno())
            # 读取一个字符
            ch = sys.stdin.read(1)
            breakflag = True if ch else False

        # 创建线程
        getchar = threading.Thread(target=getChar)
        # 设置为守护线程
        getchar.daemon = True
        # 启动守护线程
        getchar.start()
        # 输出的字符画行数
        rows = len(self.charVideo[0])//os.get_terminal_size()[0]
        for frame in self.charVideo:
            # 接收到输入则退出循环
            if breakflag is True:
                break
            self.streamOut(frame)
            self.streamFlush()
            time.sleep(self.timeInterval)
            # 共 rows 行,光标上移 rows-1 行回到开始处
            self.streamOut('\033[{}A\r'.format(rows-1))
        # 恢复标准输入为原来的属性
        termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
        # 光标下移 rows-1 行到最后一行,清空最后一行
        self.streamOut('\033[{}B\033[K'.format(rows-1))
        # 清空最后一帧的所有行(从倒数第二行起)
        for i in range(rows-1):
            # 光标上移一行
            self.streamOut('\033[1A')
            # 清空光标所在行
            self.streamOut('\r\033[K')
        info = 'User interrupt!\n' if breakflag else 'Finished!\n'
        self.streamOut(info)

if __name__ == '__main__':
    import argparse
    # 设置命令行参数
    parser = argparse.ArgumentParser()
    parser.add_argument('file',
                        help='Video file or charvideo file')
    parser.add_argument('-e', '--export', nargs = '?', const = 'charvideo.txt',
                        help='Export charvideo file')
    # 获取参数
    args = parser.parse_args()
    v2char = V2Char(args.file)
    if args.export:
        v2char.export(args.export)
    v2char.play()

这里发现之前笔记中使用的竟然是Hold feat. Daniela Andrade,很久没听了,又找到一首可以循环的歌。特别是B站up主剪得MAD,两年了还能找到链接,并且效果依然炸裂,绝了,感动ing。。。链接为:

https://www.bilibili.com/video/BV1zb41167Ak?spm_id_from=333.999.0.0

之前的笔记用该视频作为运行程序的素材后,启动程序,会发现等待时间也比较长:
在这里插入图片描述
待等待时间过后,我们可以看见终端呈现的效果,因为csdn最大为5M,部分效果:
在这里插入图片描述

可以看到,效果还是很好的,但不是每个都能如此优秀,比如我现在找了一个就中秋快乐四个字的写字视频,效果就很差了:
在这里插入图片描述

我想主要原因,是B站大佬制作的MAD很大,并且色域非常全,而中秋快乐艺术字,总共视频就11秒,原视频只有几百k,并且可以看到这四个字全映射成了$符号,色彩太统一,才出现了问题。

这里还有一个改进点是,没有声音录入,以及彩色字符,之前笔记里还有一篇引用文献是Python将视频转换为全字符视频(含音频),我去年做内部总结的时候,有进行过小修,代码为:

# -*- coding:utf-8 -*-
# coding:utf-8
import argparse
import os
import cv2
import subprocess
from cv2 import VideoWriter, VideoWriter_fourcc, imread, resize
from PIL import Image, ImageFont, ImageDraw

# 命令行输入参数处理
# aparser = argparse.ArgumentParser()
# aparser.add_argument('file')
# aparser.add_argument('-o','--output')
# aparser.add_argument('-f','--fps',type = float, default = 24)#帧
# aparser.add_argument('-s','--save',type = bool, nargs='?', default = False, const = True)
# 是否保留Cache文件,默认不保存

# 获取参数
# args = parser.parse_args()
# INPUT = args.file
# OUTPUT = args.output
# SAVE = args.save
# FPS = args.fps
# 像素对应ascii码


ascii_char = list("$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:oa+>!:+. ")


# ascii_char = list("MNHQ$OC67+>!:-. ")
# ascii_char = list("MNHQ$OC67)oa+>!:+. ")

# 将像素转换为ascii码
def get_char(r, g, b, alpha=256):
    if alpha == 0:
        return ''
    length = len(ascii_char)
    gray = int(0.2126 * r + 0.7152 * g + 0.0722 * b)
    unit = (256.0 + 1) / length
    return ascii_char[int(gray / unit)]


# 将txt转换为图片
def txt2image(file_name):
    im = Image.open(file_name).convert('RGB')
    # gif拆分后的图像,需要转换,否则报错,由于gif分割后保存的是索引颜色
    raw_width = im.width
    raw_height = im.height
    width = int(raw_width / 6)
    height = int(raw_height / 15)
    im = im.resize((width, height), Image.NEAREST)

    txt = ""
    colors = []
    for i in range(height):
        for j in range(width):
            pixel = im.getpixel((j, i))
            colors.append((pixel[0], pixel[1], pixel[2]))
            if (len(pixel) == 4):
                txt += get_char(pixel[0], pixel[1], pixel[2], pixel[3])
            else:
                txt += get_char(pixel[0], pixel[1], pixel[2])
        txt += '\n'
        colors.append((255, 255, 255))

    im_txt = Image.new("RGB", (raw_width, raw_height), (255, 255, 255))
    dr = ImageDraw.Draw(im_txt)
    # font = ImageFont.truetype(os.path.join("fonts","汉仪楷体简.ttf"),18)
    font = ImageFont.load_default().font
    x = y = 0
    # 获取字体的宽高
    font_w, font_h = font.getsize(txt[1])
    font_h *= 1.37  # 调整后更佳
    # ImageDraw为每个ascii码进行上色
    for i in range(len(txt)):
        if (txt[i] == '\n'):
            x += font_h
            y = -font_w
        # self, xy, text, fill = None, font = None, anchor = None,
        # *args, ** kwargs
        dr.text((y, x), txt[i], fill=colors[i])
        # dr.text((y, x), txt[i], font=font, fill=colors[i])
        y += font_w

    name = file_name
    # print(name + ' changed')
    im_txt.save(name)


# 将视频拆分成图片
def video2txt_jpg(file_name):
    vc = cv2.VideoCapture(file_name)
    c = 1
    if vc.isOpened():
        r, frame = vc.read()
        if not os.path.exists('Cache'):
            os.mkdir('Cache')
        os.chdir('Cache')
    else:
        r = False
    while r:
        cv2.imwrite(str(c) + '.jpg', frame)
        txt2image(str(c) + '.jpg')  # 同时转换为ascii图
        r, frame = vc.read()
        c += 1
    os.chdir('..')
    return vc


# 将图片合成视频
def jpg2video(outfile_name, fps):
    fourcc = VideoWriter_fourcc(*"MJPG")

    images = os.listdir('Cache')
    im = Image.open('Cache/' + images[0])
    vw = cv2.VideoWriter(outfile_name + '.avi', fourcc, fps, im.size)

    os.chdir('Cache')
    for image in range(len(images)):
        # Image.open(str(image)+'.jpg').convert("RGB").save(str(image)+'.jpg')
        frame = cv2.imread(str(image + 1) + '.jpg')
        vw.write(frame)
        # print(str(image + 1) + '.jpg' + ' finished')
    os.chdir('..')
    vw.release()


# 递归删除目录
def remove_dir(path):
    if os.path.exists(path):
        if os.path.isdir(path):
            dirs = os.listdir(path)
            for d in dirs:
                if os.path.isdir(path + '/' + d):
                    remove_dir(path + '/' + d)
                elif os.path.isfile(path + '/' + d):
                    os.remove(path + '/' + d)
            os.rmdir(path)
            return
        elif os.path.isfile(path):
            os.remove(path)
        return


# 调用ffmpeg获取mp3音频文件
def video2mp3(file_name):
    outfile_name = file_name.split('.')[0] + '.mp3'
    subprocess.call('ffmpeg -i ' + file_name + ' -f mp3 ' + outfile_name, shell=True)


# 合成音频和视频文件
def video_add_mp3(file_name, mp3_file):
    outfile_name = file_name.split('.')[0] + '-txt.mp4'
    subprocess.call('ffmpeg -i ' + file_name + ' -i ' + mp3_file + ' -strict -2 -f mp4 ' + outfile_name, shell=True)


if __name__ == '__main__':
    INPUT = "1.mp4"
    OUTPUT = "1.mp4"
    SAVE = "."
    FPS = "25"

    vc = video2txt_jpg(INPUT)
    FPS = vc.get(cv2.CAP_PROP_FPS)  # 获取帧率
    print(FPS)

    vc.release()

    jpg2video(INPUT.split('.')[0], FPS)
    print(INPUT, INPUT.split('.')[0] + '.mp3')
    video2mp3(INPUT)
    video_add_mp3(INPUT.split('.')[0] + '.avi', INPUT.split('.')[0] + '.mp3')

    if (not SAVE):
        remove_dir("Cache")
        os.remove(INPUT.split('.')[0] + '.mp3')
        os.remove(INPUT.split('.')[0] + '.avi')

然后启动:
在这里插入图片描述

最后,当然得以贺岁新春作为结束,所以,我打算用欣小萌于2019年9月12号中秋节前夕的视频作为结束,以此来见证中秋节的美。

视频链接为:https://www.bilibili.com/video/BV1sJ411P7CF?spm_id_from=333.337.search-card.all.click
(PS:不愧为镇站之作)

在这里插入图片描述
加上声音,如果按原视频1分45秒来算,字符输出后,大小直奔1个G,所以我裁剪为了1分钟,但还是很大,上述是压缩在5M以内的图像,另外,我将本篇中用到的基本上所有文件都打了个包,除了之前笔记里提到的视频外,其余部分大致为:
在这里插入图片描述

分享链接为:

链接:https://pan.baidu.com/s/1722im0N2Ka0eh_GXtMSWmQ?pwd=1kmj
提取码:1kmj

  • 7
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

submarineas

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值