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)
最終效果
  • 最后你可以優化一下字符的替換方式,去掉某些顯示效果不好的字符。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容