目录

Python生成字符动画

前段时间(指去年)发现咒术回战的ED做的很棒,翻来覆去看了几遍之后想把它魔改成字符画的形式,拖了快半年终于在这周做出来了!

主要思路参考了B站UP主“奇乐编程学院”的视频:【编程三分钟】教大家用Python做出漂亮的字符动画!,最终效果点这里

接下来对整个制作过程进行简单梳理,希望能对其他人有所帮助。

主要工具

整个制作过程主要用到了Python、FFmpeg两个工具,前者用于实现转换算法,后者用于视频处理。算法实现所需Python第三方库是PIL和numpy。

工具的安装方法可以很容易地在互联网上找到。

实现思路与代码

转换的整体思路分为以下三步:

  1. 从原本彩色视频中逐帧提取图片
  2. 对提取到的图片进行转换
  3. 将转换后的图片拼接成视频

图片提取

FFmpeg提取图片非常简单,在准备好视频文件,并创建文件夹用于存放提取出的图片之后,执行下面的命令便能进行提取:

ffmpeg -i ed.flv -r 25 -qscale:v 2 source/%07d.jpg

ed.flv为视频文件,25为提取帧率,source/%07d.jpg为生成文件对应路径及文件名格式,按照我的写法,第一张图片的相对路径应该是./source/0000001.jpg

图片转换

首先将图片按照一定比例进行缩放,降低像素数目,之后将图片转化为灰度图,根据每个像素亮度值占整张图片亮度区间的比例选择合适的字符,在输出图片中用每个像素对应的字符代替原有像素点。

举一个例子,假如转化为灰度图后某个像素值为0,也就是黑色,那么就可以用空格字符来代替这个像素,如果某个像素值为200,一个比较亮的值,那么就可以用'#'、‘M’这样构成像素比较多的字符来表示。

另外,如果在转化灰度图之前保存每个像素的颜色并在最后绘制字符时使用对应位置的颜色,就可以得到彩色效果的图片。

from PIL import Image, ImageDraw, ImageFont
import numpy as np
import os

sample_rate = 0.07


def ascii_art(file, source_folder, aim_folder):
    im = Image.open(source_folder + 
    '/' + file)

    # Compute letter aspect ratio
    # font = ImageFont.load_default()
    font = ImageFont.truetype("SourceCodePro-Bold.ttf", size=38)
    aspect_ratio = font.getsize("x")[0] / font.getsize("x")[1]
    new_im_size = np.array(
        [im.size[0] * sample_rate, im.size[1] * sample_rate * aspect_ratio]
    ).astype(int)

    # Downsample the image
    im = im.resize(new_im_size)

    # Keep a copy of image for color sampling
    im_color = np.array(im)

    # Convert to gray scale image
    im = im.convert("L")

    # Convert to numpy array for image manipulation
    im = np.array(im)

    # Defines all the symbols in ascending order that will form the final ascii
    s = "8&WM#*ozh"
    # s = "$@B%8&WM#*oahkbdpqw mZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_ +~<>i!lI;:,\"^`'. "
    # s = "BMOx. "
    symbols = np.array(list(reversed(s)))

    # Normalize minimum and maximum to [0, max_symbol_index)
    im = (im - im.min()) / (im.max() - im.min()) * (symbols.size - 1)

    # Generate the ascii art
    ascii = symbols[im.astype(int)]

    # Create an output image for drawing ascii text
    letter_size = font.getsize("x")
    im_out_size = new_im_size * letter_size
    bg_color = "gray"
    im_out = Image.new("RGB", tuple(im_out_size), bg_color)
    draw = ImageDraw.Draw(im_out)

    # Draw text
    y = 0
    for i, line in enumerate(ascii):
        for j, ch in enumerate(line):
            color = tuple(im_color[i, j])  # sample color from original image
            # color = "white"
            draw.text((letter_size[0] * j, y), ch[0], fill=color, font=font)
        y += letter_size[1]  # increase y by letter height

    # Save image file
    im_out.save(aim_folder + '/' + file)


source_folder = "source"
aim_folder = "out"
source_file = [f for f in os.listdir(source_folder)]
count = 0
for f in source_file:
    ascii_art(f, source_folder, aim_folder)
    count += 1
    print(count)

上面代码主体部分来自奇乐编程学院,我进行了简单的修改,指定原图片路径和输出路径,便能对路径下所有的图片进行转化,可以对采样率、绘制字体大小、替换字符串进行调整得到更好的效果。

图片合成

合成图片同样使用FFmpeg实现,执行下面的命令便能得到视频文件。

ffmpeg -i out/%07d.jpg -c:v libx264 -vf fps=25 -pix_fmt yuv420p out.mp4

实现过程中,由于参数不同,我最后得到的图片高度为奇数,执行命令后报告了高度无法被2整除的错误,在网上查找之后改用下面的命令解决:

ffmpeg -i out/%07d.jpg -c:v libx264 -vf "pad=ceil(iw/2)*2:ceil(ih/2)*2" -r 25 -pix_fmt yuv420p out.flv

总结

本来我是计划做成黑白字符效果的,但可能是因为咒术回战ED主要用不同色块进行构图,导致亮度变化比较少,转化成字符画之后感觉比较怪,但是我又说不出来问题在哪,所以改成了彩色,看起来会舒服一些。

实现过程中我使用了不同的背景色进行测试,发现灰色会有比较好的效果,但是看起来还是感觉灰蒙蒙的,或许可以通过调整字符绘制颜色来得到更好的效果。

上面问题的主要原因是字符与字符之间存在间隔,导致存在“漏光”现象,当存在连续的面积较小的字符时,主要观察到的是背景颜色。

另外,网上其他字符画的教程都使用了比较长的替换字符串,但我感觉那样做出来的效果会比较乱,所以就使用了比较短的替换字符串,感觉最后的效果还可以。