看了用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 = '''
pre {font-size:14px; line-height:14px}
 
  
 
'''
foot = '''
'''
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 = '''
pre {font-family:simsun;font-size:14px; line-height:14px}
 
  
 
'''
foot = '''
'''
with open('1.html','w') as f:
f.write(head)
import html
f.write(html.escape(resulttext))
f.write(foot)

多层次字符替换

然后就是对所有图片进行转换了,利用glob找出工作目录images文件夹下的所有bmp图片,依次处理。所有字符图片存于html文件中的



标签中,一个标签对应一个图,然后在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 = '''
pre {display:none;font-family:simsun;font-size:14px; line-height:14px}
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)
}
'''
foot = '''
'''
with open('2.html','w') as f:
f.write(head)
for resulttext in result:
f.write("
 
 
")
 
 
f.write(html.escape(resulttext))
f.write("
") 
 
f.write(foot)

最终效果

最后你可以优化一下字符的替换方式,去掉某些显示效果不好的字符。