這篇文章是 視頻轉字符動畫-Python-60行代碼 的后續,如果感興趣,請先看看它。
0. 話說在前頭
最新版使用了畫布方式實現,和本文相比改動非常大,如果對舊版本的實現沒啥興趣,可以直接移步 video2chars,它的效果動畫見 極樂凈土。新版本的核心代碼不算注釋70行不到,功能更強大。
下面的效果動畫是使用 html 實現的字符動畫效果(上一篇的效果動畫是 shell 版的):

本文的優化仍然是針對 shell 版本的,html 版由於缺陷太大就不寫文章介紹了。
1. 速度優化
要是每次播放都要等個一分鍾,也太痛苦了一點。
所以可以用 pickle 模塊把 video_chars 保存下來,下次播放時,如果發現當前目錄下有這個保存下來的數據,就跳過轉換,直接播放了。這樣就快多了。
只需要改一下測試代碼,
先在開頭添加兩個依賴
import os
import pickle
然后在文件結尾添加代碼:
def dump(obj, file_name):
"""
將指定對象,以file_nam為名,保存到本地
"""
with open(file_name, 'wb') as f:
pickle.dump(obj, f)
return
def load(filename):
"""
從當前文件夾的指定文件中load對象
"""
with open(filename, 'rb') as f:
return pickle.load(f)
def get_file_name(file_path):
"""
從文件路徑中提取出不帶拓展名的文件名
"""
# 從文件路徑獲取文件名 _name
path, file_name_with_extension = os.path.split(file_path)
# 拿到文件名前綴
file_name, file_extension = os.path.splitext(file_name_with_extension)
return file_name
def has_file(path, file_name):
"""
判斷指定目錄下,是否存在某文件
"""
return file_name in os.listdir(path)
def get_video_chars(video_path, size):
"""
返回視頻對應的字符視頻
"""
video_dump = get_file_name(video_path) + ".pickle"
# 如果 video_dump 已經存在於當前文件夾,就可以直接讀取進來了
if has_file(".", video_dump):
print("發現該視頻的轉換緩存,直接讀取")
video_chars = load(video_dump)
else:
print("未發現緩存,開始字符視頻轉換")
print("開始逐幀讀取")
# 視頻轉字符動畫
imgs = video2imgs(video_path, size)
print("視頻已全部轉換到圖像, 開始逐幀轉換為字符畫")
video_chars = imgs2chars(imgs)
print("轉換完成,開始緩存結果")
# 把轉換結果保存下來
dump(video_chars, video_dump)
print("緩存完畢")
return video_chars
if __name__ == "__main__":
# 寬,高
size = (64, 48)
# 視頻路徑,換成你自己的
video_path = "BadApple.mp4"
video_chars = get_video_chars(video_path, size)
play_video(video_chars)
另一個優化方法就是邊轉換邊播放,就是同時執行上述三個步驟。學會了的話,可以自己實現一下試試。
2. 字符視頻和音樂同時播放
沒有配樂的動畫,雖然做出來了是很有成就感,但是你可能看上兩遍就厭倦了。
所以讓我們來給它加上配樂。(不要擔心,其實就只需要添加幾行代碼而已)
首先我們需要找個方法來播放視頻的配樂,怎么做呢?
先介紹一下一個跨平台視頻播放器:mpv,它有很棒的命令行支持,請先安裝好它。
要讓 mpv 只播放視頻的音樂部分,只需要命令:
mpv --no-video video_path
好了,現在有了音樂,可總不能還讓人開倆shell,先放音樂,再放字符畫吧。
這時候,我們需要的功能是:使用 Python 調用外部應用.
但是 mpv 使用了類似 curses 的功能,標准庫的 os.system 不能隱藏掉這個部分,播放效果不盡如人意。
因此我使用了 pyinvoke 模塊,只要給它指定參數hide=True,就可以完美隱藏掉被調用程序的輸出(指 stdout,其實 subprocess 也可以的)。運行下面代碼前,請先用pip安裝好 invoke.(能夠看到這里的,安裝個模塊還不是小菜一碟)
好了廢話說這么多,上代碼:
import invoke
video_path = "BadApple.mp4"
invoke.run(f"mpv --no-video {video_path}", hide=True, warn=True)
運行上面的測試代碼,如果聽到了音樂,而shell啥都沒輸出,但是能聽到音樂的話,就正常了。我們繼續。(這里使用了python3.6的f字符串)
音樂已經有了,那就好辦了。
添加一個播放音樂的函數
import invoke
def play_audio(video_path):
invoke.run(f"mpv --no-video {video_path}", hide=True, warn=True)
然后修改main()方法:
def main():
# 寬,高
size = (64, 48)
# 視頻路徑,換成你自己的
video_path = "BadApple.mp4"
# 只轉換三十秒,這個屬性是才添加的,但是上一篇的代碼沒有更新。你可能需要先上github看看最新的代碼。其實就稍微改了一點。
seconds = 30
# 這里的fps是幀率,也就是每秒鍾播放的的字符畫數。用於和音樂同步。這個更新也沒寫進上一篇,請上github看看新代碼。
video_chars, fps = get_video_chars(video_path, size, seconds)
# 播放音軌
play_audio(video_path)
# 播放視頻
play_video(video_chars, fps)
if __name__ == "__main__":
main()
然后運行。。並不是我坑你,你只聽到了聲音,卻沒看到字符畫。。原因是: invoke.run()函數是阻塞的,音樂沒放完,代碼就到不了play_video(video_chars, fps)這一行。
所以 play_audio 還要改一下,改成這樣:
import invoke
from threading import Thread
def play_audio(video_path):
def call():
invoke.run(f"mpv --no-video {video_path}", hide=True, warn=True)
# 這里創建子線程來執行音樂播放指令,因為 invoke.run() 是一個阻塞的方法,要同時播放字符畫和音樂的話,就要用多線程/進程。
# P.S. 更新:現在發現可以用 subprocess.Popen 實現異步調用 mpv,不需要開新線程。有興趣的同學可以自己試試。
p = Thread(target=call)
p.setDaemon(True)
p.start()
這里使用標准庫的 threading.Thread 類來創建子線程,讓音樂的播放在子線程里執行,然后字符動畫還是主線程執行,Ok,這就可以看到最終效果了。實際上只添加了十多行代碼而已。
3. 彩色字符動畫
- html+javascript 方式:核心都是一樣的內容,只是需要點 html 和 javascript 的知識。代碼見 video2chars-html
- 畫布方式:直接把畫在圖片上,然后自動合成為 mp4 文件。這種方式要優於 html 方式,而且有個很方便的庫能用,核心代碼就 70 行的樣子。代碼見 video2chars
