看了用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)
最終效果
- 最后你可以優化一下字符的替換方式,去掉某些顯示效果不好的字符。