昨晚一朋友跟我說在網上看到了別人做的視頻轉字符動畫,覺得很厲害,我於是也打算玩玩。今天中午花時間實現了這樣一個小玩意。
順便把過程記錄在這里。
注:最新版使用了畫布方式實現,和本文相比改動非常大,如果對舊版本的實現沒啥興趣,可以直接移步 video2chars,它的效果動畫見 極樂凈土。新版本的核心代碼不算注釋70行不到,功能更強大。
效果
先上效果,來點動力:
- 源視頻: BadApple.mp4
- 轉換后:
步驟
- 將視頻轉化為一幀一幀的圖片
- 把圖片轉化為字符畫
- 按順序播放字符畫
一、准備
1. 模塊
這個程序需要用到這樣幾個模塊:
- opencv-python # 用來讀取視頻和圖片
- numpy # opencv-python 依賴於它
准備階段,首先安裝依賴:
pip3 install numpy opencv-python
然后新建python代碼文檔,在開頭添加上下面的導入語句
#-*- coding:utf-8 -*-
# numpy 是一個矩陣運算庫,圖像處理需要用到。
import numpy as np
2. 材料
材料就是需要轉換的視頻文件了,我這里用的是BadApple.mp4,下載下來和代碼放到同一目錄下
你也可以換成自己的,建議是學習時盡量選個短一點的視頻,幾十秒就行了,不然調試起來很痛苦。(或者自己稍微修改一下函數,只轉換一定范圍、一定數量的幀。)
此外,要選擇對比度高的視頻。否則的話,就需要彩色字符才能有足夠好的表現,有時間我試試。
二、按幀讀取視頻
現在繼續添加代碼,實現第一步:按幀讀取視頻。
下面這個函數,接受視頻路徑和字符視頻的尺寸信息,返回一個img列表,其中的img是尺寸都為指定大小的灰度圖。
#導入 opencv
import cv2
def video2imgs(video_name, size):
"""
:param video_name: 字符串, 視頻文件的路徑
:param size: 二元組,(寬, 高),用於指定生成的字符畫的尺寸
:return: 一個 img 對象的列表,img對象實際上就是 numpy.ndarray 數組
"""
img_list = []
# 從指定文件創建一個VideoCapture對象
cap = cv2.VideoCapture(video_name)
# 如果cap對象已經初始化完成了,就返回true,換句話說這是一個 while true 循環
while cap.isOpened():
# cap.read() 返回值介紹:
# ret 表示是否讀取到圖像
# frame 為圖像矩陣,類型為 numpy.ndarry.
ret, frame = cap.read()
if ret:
# 轉換成灰度圖,也可不做這一步,轉換成彩色字符視頻。
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# resize 圖片,保證圖片轉換成字符畫后,能完整地在命令行中顯示。
img = cv2.resize(gray, size, interpolation=cv2.INTER_AREA)
# 分幀保存轉換結果
img_list.append(img)
else:
break
# 結束時要釋放空間
cap.release()
return img_list
寫完后可以寫個main方法測試一下,像這樣:
if __name__ == "__main__":
imgs = video2imgs("BadApple.mp4", (64, 48))
assert len(imgs) > 10
如果運行沒報錯,就沒問題
代碼里的注釋應該寫得很清晰了,繼續下一步。
三、圖像轉化為字符畫
視頻轉換成了圖像,這一步便是把圖像轉換成字符畫
下面這個函數,接受一個img對象為參數,返回對應的字符畫。
# 用於生成字符畫的像素,越往后視覺上越明顯。。這是我自己按感覺排的,你可以隨意調整。
pixels = " .,-'`:!1+*abcdefghijklmnopqrstuvwxyz<>()\/{}[]?234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ%&@#$"
def img2chars(img):
"""
:param img: numpy.ndarray, 圖像矩陣
:return: 字符串的列表:圖像對應的字符畫,其每一行對應圖像的一行像素
"""
res = []
# 灰度是用8位表示的,最大值為255。
# 這里將灰度轉換到0-1之間
# 使用 numpy 的逐元素除法加速,這里 numpy 會直接對 img 中的所有元素都除以 255
percents = img / 255
# 將灰度值進一步轉換到 0 到 (len(pixels) - 1) 之間,這樣就和 pixels 里的字符對應起來了
# 同樣使用 numpy 的逐元素算法,然后使用 astype 將元素全部轉換成 int 值。
indexes = (percents * (len(pixels) - 1)).astype(np.int)
# 要注意這里的順序和 之前的 size 剛好相反(numpy 的 shape 返回 (行數、列數))
height, width = img.shape
for row in range(height):
line = ""
for col in range(width):
index = indexes[row][col]
# 添加字符像素(最后面加一個空格,是因為命令行有行距卻沒幾乎有字符間距,用空格當間距)
line += pixels[index] + " "
res.append(line)
return res
上面的函數只接受一幀為參數,一次只轉換一幀,可我們需要的是轉換所有的幀,所以就再把它包裝一下:
def imgs2chars(imgs):
video_chars = []
for img in imgs:
video_chars.append(img2chars(img))
return video_chars
好了,現在我們可以測試一下:
if __name__ == "__main__":
imgs = video2imgs("BadApple.mp4", (64, 48))
video_chars = imgs2chars(imgs)
assert len(video_chars) > 10
沒報錯的話,就可以下一步了。(這一步比較慢,測試階段建議用短一點的視頻,或者稍微改一下,只處理前30秒之類的)
四、播放字符視頻
寫了這么多代碼,現在終於要出成果了。現在就是最激動人心的一步:播放字符畫了。
同樣的,我把它封裝成了一個函數。下面這個函數接受一個字符畫的列表並播放。
- 通用版(使用 shell 的 clear 命令清屏,但是因為效率不高,可能會有一閃一閃的問題)
這個版本適用於 linux/windows
# 導入需要的模塊
import time
import subprocess
def play_video(video_chars):
"""
播放字符視頻
:param video_chars: 字符畫的列表,每個元素為一幀
:return: None
"""
# 獲取字符畫的尺寸
width, height = len(video_chars[0][0]), len(video_chars[0])
for pic_i in range(len(video_chars)):
# 顯示 pic_i,即第i幀字符畫
for line_i in range(height):
# 將pic_i的第i行寫入第i列。
print(video_chars[pic_i][line_i])
time.sleep(1 / 24) # 粗略地控制播放速度。
# 調用 shell 命令清屏
subprocess.run("clear", shell=True) # linux 版
# subrpocess.run("cls", shell=True) # cmd 版,windows 系統請用這一行。
- Unix系版本(使用了只支援 unix 系 的 curses 庫,比 clear 更流暢)
# 導入需要的模塊
import time
import curses
def play_video(video_chars):
"""
播放字符視頻,
:param video_chars: 字符畫的列表,每個元素為一幀
:return: None
"""
# 獲取字符畫的尺寸
width, height = len(video_chars[0][0]), len(video_chars[0])
# 初始化curses,這個是必須的,直接抄就行
stdscr = curses.initscr()
curses.start_color()
try:
# 調整窗口大小,寬度最好略大於字符畫寬度。另外注意curses的height和width的順序
stdscr.resize(height, width * 2)
for pic_i in range(len(video_chars)):
# 顯示 pic_i,即第i幀字符畫
for line_i in range(height):
# 將pic_i的第i行寫入第i列。(line_i, 0)表示從第i行的開頭開始寫入。最后一個參數設置字符為白色
stdscr.addstr(line_i, 0, video_chars[pic_i][line_i], curses.COLOR_WHITE)
stdscr.refresh() # 寫入后需要refresh才會立即更新界面
time.sleep(1 / 24) # 粗略地控制播放速度(24幀/秒)。更精確的方式是使用游戲編程里,精靈的概念
finally:
# curses 使用前要初始化,用完后無論有沒有異常,都要關閉
curses.endwin()
return
好,接下來就是見證奇跡的時刻
不過開始前要注意,字符畫的播放必須在shell窗口下運行,在pycharm里運行會看到一堆無意義字符。另外播放前要先最大化shell窗口
if __name__ == "__main__":
imgs = video2imgs("BadApple.mp4", (64, 48))
video_chars = imgs2chars(imgs)
input("`轉換完成!按enter鍵開始播放")
play_video(video_chars)
寫完后,開個shell,最大化窗口,然后鍵入(文件名換成你的)
python3 video2chars.py
可能要等很久。我使用示例視頻大概需要 12 秒左右。看到提示的時候,按回車,開始播放!
**這樣就完成了視頻到字符動畫的轉換, 除去注釋, 大概七十行代碼的樣子. 稍微超出了點預期, 不過效果真是挺棒的. **
五、進一步優化
到了這里,核心功能基本都完成了。
不過仔細想想,其實還有很多可以做的:
- 能不能手動指定要轉換的區間、幀率?
- 每次轉換都要很久的時間,能不能邊轉換邊播放?或者轉換后把數據保存起來,下次播放時,就直接讀緩存。
- 為啥我的字符動畫沒有聲音,看無聲電影么?
- 視頻的播放速度能不能精確控制?
- 能不能用彩色字符?
這些東西,就不寫這里了,再寫下去,你們肯定要說我這標題是騙人了哈哈。
所以如果有興趣的,請移步這個系列的下一篇:Python 視頻轉字符畫 - 進階
六、總結
完整代碼見 video2chars.py,要注意的是代碼庫的代碼,包含了第二篇文章的內容(音頻、緩存、幀率控制等),而且相對這篇文章也有一些小改動(目的是方便使用,但是稍微增加了點代碼量,所以改動沒有寫在這篇文章里了)
想運行起來的話,還是建議跟着文章做。。
七、參考
允許轉載, 但是要求附上來源鏈接: 視頻轉字符動畫-Python-60行代碼