捣鼓了几天python代码,我现在也越来越发现python的魅力所在,它的强大之处在于有非常多的第三方库可以随意调用。我不需要知道这些第三方库各种函数的实现方式,只要知道这些函数有什么作用,能得到什么结果。只要构思好自己的想法,找到对应的库就可以一步步按照我的思路编写程序,实现我想要的结果,整个构思到实现的过程让我非常愉悦~
写这篇博客纯粹是个人爱好,也是一个巧合~
前几天刷b站看到有人做了个剪影的字符动画,我就很好奇python是否可以实现。参考了一下github上大佬们的图片转字符画的代码,对这些代码做了点深入研究,总算搞明白了其实现方式,并且自己动手修改代码,在原有基础上改了几个bug,新增几个模块的调用,最后一步封装写成了下面这个脚本。这个脚本的功能是只要输入视频文件和你想要的视频帧率,就可以自动将视频转化为字符动画 。
可以先看一下视频效果~ 或者点击这里进入b站观看
Your browser does not support the video tag.
思路很清晰,接下来是写代码实现的过程,细节方面需要调用其他库
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 from PIL import Image, ImageDraw, ImageFont import subprocess import sysimport os import shutil import numpy as np import gc file_input = sys.argv[1 ] FPS = sys.argv[2 ] def do_turn (file_input, FPS ): os.makedirs("tempfile/cut/" ) shell_vedio = "ffmpeg -i " + file_input + " -r " + FPS + " -qscale:v 2 ./tempfile/cut/%05d.jpg" shell_voice = "ffmpeg -i " + file_input + " ./tempfile/out.mp3" subprocess.call(shell_vedio, shell=True ) subprocess.call(shell_voice, shell=True ) count =0 for file in os.listdir("./tempfile/cut/" ): count += 1 print ("成功分离音频,截图开始转换字符画......" + "共计" + str (count) + "张" ) list_p = os.listdir("./tempfile/cut/" ) cwd = os.getcwd() os.mkdir("./tempfile/new/" ) process = 1 for id in list_p: address = str ("" .join(cwd + '/tempfile/cut/' + id )) im = Image.open (address) font = ImageFont.truetype("DejaVuSans-Bold" , size=20 ) rate = 0.1 aspect_ratio = font.getsize("x" )[0 ] / font.getsize("x" )[1 ] new_im_size = np.array([im.size[0 ] * rate, im.size[1 ] * rate * aspect_ratio]).astype(int ) im = im.resize(new_im_size) im = np.array(im.convert("L" )) symbols = np.array(list (" .-vM@" )) if im.max () == im.min (): if im.max () > 0 : im = (im / im) * (symbols.size - 1 ) else : im[np.isnan(im)] = 0 else : im = (im - im.min ()) / (im.max () - im.min ()) * (symbols.size - 1 ) ascii = symbols[im.astype(int )] letter_size = font.getsize("x" ) im_out_size = new_im_size * letter_size im_out = Image.new("RGB" , tuple (im_out_size), "black" ) draw = ImageDraw.Draw(im_out) y = 0 for i, m in enumerate (ascii ): for j, n in enumerate (m): draw.text((letter_size[0 ] * j, y), n, font=font) y += letter_size[1 ] im_out.save("./tempfile/new/" + id + ".png" ) print (address + "转换成功!当前进度:" + str (process) + "/" + str (count)) process += 1 gc.collect() print ("转换成功!开始生成视频,请稍候......" ) outvedio = "ffmpeg -r " + FPS + " -i ./tempfile/new/%05d.jpg.png ./tempfile/out.mp4" subprocess.call(outvedio, shell=True ) final_vedio = "ffmpeg -i ./tempfile/out.mp4 -i ./tempfile/out.mp3 final.mp4" subprocess.call(final_vedio, shell=True ) shutil.rmtree("./tempfile" ) print ("字符动画final.mp4已生成!已移除临时文件夹" ) def usage (): print ("usage:" , sys.argv[0 ], "<file_input> <FPS>" ) exit(0 ) if __name__ == "__main__" : if len (sys.argv) != 3 : usage() else : do_turn(file_input, FPS)
调用numpy模块生成数组,是因为python本身虽然可以建立多维度的数组,但是书写起来非常麻烦。numpy可以很好地解决这个问题,可以理解为能构建一个更好用的数组。在对数组进行遍历穷举,要注意两次穷举分别生成两个数组 ,第二次生成的数组只有一个数,所以下面draw.text第二个参数text可以用n,也可以用n[0]列举第一个数。
1 2 3 4 5 y = 0 for i, m in enumerate (ascii ): for j, n in enumerate (m): draw.text((letter_size[0 ] * j, y), n, font=font) y += letter_size[1 ]
字符大小,字符格式,以什么字符为参照,都是可以调整的。只要注意一点,我们是按照像素点的亮度来赋于这个像素点用什么字符的,所以索引列比较重要,要自己按照字符亮度排序,添加字符注意改值。
1 symbols = np.array(list (" .-vM@" ))
还有,在计算亮度和赋予索引值的时候,我们是按照相对亮度来计算的。因此,当图片所有像素点都是一种颜色的时候,im.max() 和 im.min()值是相等的,相对亮度会出现0/0的值,导致报错。所以我加了以下判断条件:纯色黑色和其他颜色属于两种不同情况,黑色时numpy数组亮度是非数值NaN,需要将数组全部值进行替换为亮度最小的字符的值;因为是RGB取值,其他颜色值固定在0-255之间,颜色均一,相对亮度就没有意义了,因此全部调整为最亮字符的值。
1 2 3 4 5 6 7 if im.max () == im.min (): if im.max () > 0 : im = (im / im) * (symbols.size - 1 ) else : im[np.isnan(im)] = 0 else : im = (im - im.min ()) / (im.max () - im.min ()) * (symbols.size - 1 )
顺便再说一个很有意思的模块subprocess,subprocess.call()函数可以执行命令行的命令,并且这个命令是在子进程实行的,只有子程序结束才会继续执行其他命令,使用起来真的特别方便!比如有些程序我的python库里没有但是我的linux里有,在python脚本的某一步我需要用到linux里的软件去处理,这个时候就可以调用subprocess.call()函数去执行linux命令行的命令了。
还有一个函数虽然不显眼,但是起着至关重要的作用 gc.collect()
没有这个函数部分运行内存不够的电脑会崩……我在这里踩了个大坑……
在对程序进行简化以后,我以为优化地差不多了,然后发现有的时候程序会被莫名其妙killed……
vi /var/log/messages
查看运行日志,好家伙,内存溢出了
经过一番度娘,我检查了一下自己也没有用到循环引用的变量啊,那么真相只有一个了:转换字符画部分程序有3个for循环嵌套,可能是for循环引用的对象没有及时回收导致内存不断增长,最后被系统kill掉(个人猜测) 。虽然python本身有垃圾回收功能,而在程序运行的时候清理地并不是很及时,引入的gc模块是python的垃圾收集器模块,与默认的回收方式算法不同,gc.collect()
函数可以强制进行垃圾回收。因此我在每个转化字符画的for循环执行一次结束后强制回收内存,如下:
效果是立竿见影的,内存占用再也没超过5%了,非常的稳定!
缺什么第三方库就装什么,主要是pillow库、numpy库和ffmpeg,用conda可以直接安装。上面那段脚本代码复制粘贴,保存为ascii.py,运行命令:
python ascii.py <视频文件> <你想要的视频帧率>
回车,OK,静静等屏幕上的提示就好了。视频文件不在当前文件夹的话自行加上绝对路径,完成以后只会在当前目录 生成一个out.mp4的输出文件。
源代码将同步上传我的github 。