python 将视频转化为字符画之badapple

看了用PYTHON制作字符动画演示科技bilibili哔哩哔哩弹幕视频网后觉得挺好玩,复现了一下,整体思路很简单,将视频分解为图片,然后将图片逐一转换为字符画,然后利用浏览器进行逐帧播放。

浏览器播放效果
  • 首先安装FFmpeg,一款开源的视频软件,有丰富的视频处理功能。如何在Windows上安装FFmpeg程序

  • 然后使用window下的批处理batch对视频抓帧,在工作目录下新建run.bat

mkdir images
set /p input="input file:"
set /p rate="set frame rate(Hz value, fraction or abbreviation):"
set /p output="output file:"
ffmpeg -i %input% -r %rate% %output%

然后双击该bat运行,在第一句后输入被转化视频名称,在第二句后指定抓帧频率,为33.333(与后文代码中的播放间隔相对应),在第三句后输入images/%d.bmp,将所有转化图片放于工作目录下的images文件夹中。

抓帧

  • 测试单张转换。在这里,仅仅先将图片转换为灰阶,然后对每个像素点做判断,根据黑白分别转化为'@'' '(空格),最后输出文本到html中。
 import os
os.chdir(r'F:\badapple!!\images') # 转移到工作目录
from PIL import Image, ImageFont, ImageDraw
originalImage = Image.open('6382.bmp')# 图片尺寸为(480, 360)
grayImage = originalImage.resize((120, int(90/2))).convert('L')
# 将图片尺寸缩小,即减少像素点,并转换为灰阶
# int(90/2) 因为字符的高约是宽的两倍,由于之后是一个像素点替换为一个字符,所以提前将高缩小一倍
resulttext = ''
for row in range(grayImage.size[1]): # 先行后列
    for col in range(grayImage.size[0]):
        pixel = grayImage.getpixel((col, row))
        char = '@' if pixel < 127 else ' ' # 像素点值为0是黑色
        resulttext += char
    resulttext += '\n'
#print(resulttext)
head = '''
<html>
<head>
</head>
<style>
pre {font-size:14px; line-height:14px}
</style>
<body>
<pre>
'''
foot = '''
</pre>
</body>
</html>
'''
with open('1.html','w') as f:
    f.write(head)
    f.write(resulttext)
    f.write(foot)
单张转换效果
  • 由于上述对图像的转化只有黑白两个层次,太过简单,表现力不够丰富,所以我们将一系列字符按一定规则排序,然后匹配到相应的灰度上。我们先从网上下载一份simsun的字体文件来提供画图中的字体,然后利用PIL中的画图功能,先创建一个最大字符尺寸的矩形白板,然后在上面画上字符,然后计算它的平均像素(每个像素点*该点像素值/总像素点数),根据平均像素来排序。然后再做下单张测试。此外,由于文本输出到html文件,所以需要html.escape()函数进行转义。

补充:chr函数将数字转换为ACII码对应的字符
chr
### 然后对像素对字符的转换方式进行改进
font= ImageFont.truetype('../simsun.ttf', 14)
chars = list(chr(i) for i in range(32, 126))
sizeList = list(font.getsize(char) for char in chars)
import functools
maxSize = functools.reduce(lambda x,y:(max(x[0],y[0]), max(x[1],y[1])), sizeList)
#(8, 15)
tempCharImage = Image.new('L', maxSize, 'white')
tempCharDraw = ImageDraw.Draw(tempCharImage)
charDegreeDict = {}
for char in chars:
    tempCharDraw.rectangle([(0,0), maxSize], fill='white')#在(0,0)位置处以白色填充一个canvasSize的矩形。
    tempCharDraw.text((0,0), char, font=font)
    pixelColor = tempCharImage.getcolors()#返回当前图片上的所有色彩及其像素点数的列表,[(个数,色彩),(个数,色彩),...]
    grayDegree = sum(pixelnum*color for pixelnum,color in pixelColor)/(maxSize[0]*maxSize[1])
    charDegreeDict[char] = grayDegree
sortedCharDegreeList = sorted(charDegreeDict.items(), key=lambda d:d[1])
sortedCharDegreeList = list(i[0] for i in sortedCharDegreeList)
charsIndexMax = len(sortedCharDegreeList) -1 
# ['M', 'W', '@', 'N', 'H', '%', 'X', '$', 'B', 'K', 'R', '#', 'E', 'U', 'Q', 'D', '&', 'F', 'w', '8', 'k', '9', '6', 'h', 'm', 'A', 'P', 'p', 'd', '*', 'V', 'g', 'Y', '0', '5', 'b', 'q', 'G', 'O', 'n', 'x', 'f', 'S', 'J', 'T', 'Z', '3', 'y', 'u', 'I', 'C', 'L', '2', 'a', ']', '[', '1', '?', 'l', 's', 'e', 'r', '|', '}', '{', 't', 'z', 'o', 'v', '4', '+', '/', 'i', 'j', '=', 'c', '7', '(', ')', '!', '\\', '_', '>', '<', ':', '-', '"', "'", ',', ';', '.', '^', '`', ' ']
### 按灰度替换字符重新测试
import os
os.chdir(r'F:\badapple!!\images')
from PIL import Image, ImageFont, ImageDraw
originalImage = Image.open('6382.bmp')# 图片尺寸为(480, 360)
grayImage = originalImage.resize((120, int(90/2))).convert('L')
# 将图片尺寸缩小,即减少像素点,并转换为灰阶
# int(90/2) 因为字符的高是长的两倍,由于之后是一个像素点替换为一个字符,所以提前将高缩小一倍
resulttext = ''
for row in range(grayImage.size[1]):
    for col in range(grayImage.size[0]):
        pixel = grayImage.getpixel((col, row))
        char = sortedCharDegreeList[int(pixel/255*charsIndexMax)] # 像素点值为0是黑色,255为白色
        resulttext += char
    resulttext += '\n'
#print(resulttext)
head = '''
<html>
<head>
</head>
<style>
pre {font-family:simsun;font-size:14px; line-height:14px}
</style>
<body>
<pre>
'''
foot = '''
</pre>
</body>
</html>
'''
with open('1.html','w') as f:
    f.write(head)
    import html
    f.write(html.escape(resulttext))
    f.write(foot)
多层次字符替换
  • 然后就是对所有图片进行转换了,利用glob找出工作目录images文件夹下的所有bmp图片,依次处理。所有字符图片存于html文件中的<pre></prev>标签中,一个标签对应一个图,然后在js代码中每隔30秒进行下一张图的显示和前一张的隐藏,这样就实现了播放。

补充:glob的用法

import glob  
#获取指定目录下的所有图片  
print glob.glob(r"E:\Picture\*\*.jpg")  
#获取上级目录的所有.py文件  
print glob.glob(r'../*.py') #相对路径  
### ffmpeg -i "Touhou - Bad Apple!!  PV.webm" -f mp3 -vn apple.mp3 可以输出音频,暂时没有用到
workdir = r'F:\badapple!!\images'
### 按灰度替换的字符列表
sortedCharDegreeList = ['M', 'W', '@', 'N', 'H', '%', 'X', '$', 'B', 'K', 'R', '#', 'E', 'U', 'Q', 'D', '&', 'F', 'w', '8', 'k', '9', '6', 'h', 'm', 'A', 'P', 'p', 'd', '*', 'V', 'g', 'Y', '0', '5', 'b', 'q', 'G', 'O', 'n', 'x', 'f', 'S', 'J', 'T', 'Z', '3', 'y', 'u', 'I', 'C', 'L', '2', 'a', ']', '[', '1', '?', 'l', 's', 'e', 'r', '|', '}', '{', 't', 'z', 'o', 'v', '4', '+', '/', 'i', 'j', '=', 'c', '7', '(', ')', '!', '\\', '_', '>', '<', ':', '-', '"', "'", ',', ';', '.', '^', '`', ' ']
charsIndexMax = len(sortedCharDegreeList) -1 
import os, glob, html
os.chdir(workdir)
from PIL import Image, ImageFont, ImageDraw
result = []
imgs = glob.glob('*.bmp')
imgs = sorted(imgs, key=lambda x: int(x.split('.')[0])) # 这里对图片路径进行了处理,取后缀前的数字值进行排序
#
for img in imgs:
    originalImage = Image.open(img)# 图片尺寸为(480, 360)
    grayImage = originalImage.resize((120, int(90/2))).convert('L')
    # 将图片尺寸缩小,即减少像素点,并转换为灰阶
    # int(90/2) 因为字符的高是长的两倍,由于之后是一个像素点替换为一个字符,所以提前将高缩小一倍
    resulttext = ''
    for row in range(grayImage.size[1]):
        for col in range(grayImage.size[0]):
            pixel = grayImage.getpixel((col, row))
            char = sortedCharDegreeList[int(pixel/255*charsIndexMax)] # 像素点值为0是黑色,255为白色
            resulttext += char
        resulttext += '\n'
    result.append(resulttext)
    print(img,'is done!')
#
head = '''
<html>
<head>
</head>
<style>
pre {display:none;font-family:simsun;font-size:14px; line-height:14px}
</style>
<script>
window.onload = function(){
    var pres = document.getElementsByTagName('pre');
    var i = 0;
    var play = function(){
        if(i > 0){
            pres[i-1].style.display = 'none';
        }
        pres[i].style.display = 'inline-block';
        i++;
        if(i == pres.length){
            clearInterval(run)
        }
    }
    run = setInterval(play, 30)
}
</script>
<body>
'''
foot = '''
<video width="480" height="360" controls="controls" autoplay="autoplay">
  <source src="../Touhou - Bad Apple!!  PV.webm" type="video/webm" />
</video>
</body>
</html>
'''
with open('2.html','w') as f:
    f.write(head)
    for resulttext in result:
        f.write("<pre>")
        f.write(html.escape(resulttext))
        f.write("</pre>")
    f.write(foot)
最终效果
  • 最后你可以优化一下字符的替换方式,去掉某些显示效果不好的字符。

推荐阅读更多精彩内容