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


    引言

    本篇博文写作初衷是看到中秋活动,正好之前有总结过两篇关于图片与视频转字符动画的博文,以下大部分代码与小部分说明引用自之前写过的笔记,一篇是来自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)
    """
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    确认没有问题后,就能先导入环境所需要的包,这边建议是在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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    这里需要说明的是,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")
    
    • 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

    然后运行代码,结果为:
    在这里插入图片描述
    完全看不出是什么东西,因为我们只是对于颜色编码空间进行了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")
    
    • 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

    因为有用到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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 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(立方插值)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

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

    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)
    
    
    • 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

    这个类有两个主要方法,一个是从视频文件转为字符动画的方法 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()
    
    • 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

    这里发现之前笔记中使用的竟然是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')
    
    
    • 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

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

    最后,当然得以贺岁新春作为结束,所以,我打算用欣小萌于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

  • 相关阅读:
    使用遗传算法优化模糊隶属函数以实现最优模糊控制
    Shelby American 汽车 NFT 系列来袭!
    手麻系统全套源码 术前、术中、术后全过程管理
    npm下载包速度慢-淘宝NPM镜像服务器--如何切换其他服务器下载
    【Java核心】JDK、JRE、 JVM的联系与区别
    MySQL索引、事务、存储引擎
    撰寫自己的Python C擴展!
    Linux内核代码中常用的数据结构
    R语言中的取整函数
    Assimp库模型导入结构
  • 原文地址:https://blog.csdn.net/submarineas/article/details/126584790