程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式師世界 >> 編程語言 >  >> 更多編程語言 >> Python

Python 視頻轉字符畫 - 進階

編輯:Python

昨晚我在網上看到了別人做的視頻轉字符動畫,覺得很厲害,我於是也打算玩玩。今天中午花時間實現了這樣一個小玩意。

順便把過程記錄在這裡。

1.源視頻:

2.轉換後:

步驟

1、將視頻轉化為一幀一幀的圖片

2、把圖片轉化為字符畫

3、按順序播放字符畫

一、准備

1、模塊

這個程序需要用到這樣幾個模塊:

1. opencv-python #用來讀取視頻和圖片

2. numpy # opencv-python依賴於它

准備階段,首先安裝依賴: .

pip3 install numpy opencv-python

然後新建python代碼文檔,在開頭添加上下面的導入語句

#-*- coding:utf-8 -*-

# numpy 是一個矩陣運算庫,圖像處理需要用到。

import numpy as np

2、材料

材料就是需要轉換的視頻文件了,下載下來和代碼放到同一目錄下,你也可以換成自己的,建議是學習時盡量選個短一點的視頻,幾十秒就行了,不然調試起來很痛苦。(或者自己稍微修改一下函數,只轉換一定范圍、一定數量的幀。)此外,要選擇對比度高的視頻。否則的話,就需要彩色字符才能有足夠好的表現,有時間我試試。

二、按幀讀取視頻

現在繼續添加代碼,實現第一步:按幀讀取視頻。

下面這個函數,接受視頻路徑和字符視頻的尺寸信息,返回一個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 版

# subprocess.run("cls", shell=True) # cmd 版,windows 系統請用這一行。

好,接下來就是見證奇跡的時刻

不過開始前要注意,字符畫的播放必須在shell窗口下運行,在pycharm裡運行會看到一堆無意義字符。另外播放前要先最大化shell窗口

if __name__ == "__main__":

imgs = video2imgs("BadApple.mp4", (64, 48))

video_chars = imgs2chars(imgs)

input("`轉換完成!按enter鍵開始播放")

play_video(video_chars)

寫完後,開個shell,最大化窗口,然後鍵入(文件名換成你的)

可能要等很久。我使用示例視頻大概需要 12 秒左右。看到提示的時候,按回車,開始播放!

********這樣就完成了視頻到字符動畫的轉換, 除去注釋, 大概七十行代碼的樣子. 稍微超出了點預期, 不過效果真是挺棒的

五、速度優化

要是每次播放都要等個一分鐘,也太痛苦了一點。

所以可以用 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_extesion = os.path.split(file_path)

# 拿到文件名前綴

file_name, file_extern = os.path.splitext(file_name_with_extesion)

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)

另一個優化方法就是邊轉換邊播放,就是同時執行上述三個步驟。學會了的話,可以自己實現一下試試。

4. 字符視頻和音樂同時播放

沒有配樂的動畫,雖然做出來了是很有成就感,但是你可能看上兩遍就厭倦了。

所以讓我們來給它加上配樂。(不要擔心,其實就只需要添加幾行代碼而已),首先我們需要找個方法來播放視頻的配樂,怎麼做呢?先介紹一下一個跨平台視頻播放器:mpv,它有很棒的命令行支持,請先安裝好它。

要讓 mpv 只播放視頻的音樂部分,只需要命令:

mpv --no-video video_path

好了,現在有了音樂,可總不能還讓人開倆shell,先放音樂,再放字符畫吧。

這時候,我們需要的功能是:使用python調用外部應用

但是 mpv 使用了類似 curses 的功能,標准庫的 os.system 和 subprocess 都不能隱藏掉這個部分,播放效果不盡如人意。

因此我使用了 pyinvoke 模塊,只要給它指定參數 hide=True ,就可以完美隱藏掉被調用程序的輸出。

好了廢話說這麼多,上代碼:

import invoke

video_path = "BadApple.mp4"

 invoke.run(f"mpv --no-video {video_path}", hide=True, warn=True)

運行上面的測試代碼,如果聽到了音樂,而shell啥都沒輸出的話,就正常了。我們繼續。

音樂已經有了,那就好辦了。

添加一個播放音樂的函數

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 = Thread(target=call)

p.setDaemon(True)

p.start()

這裡使用標准庫的 threading.Thread 類來創建子線程,讓音樂的播放在子線程裡執行,然後字符動畫還是主線程執行,Ok,這就可以看到最終效果了。實際上只添加了十多行代碼而已。

六、總結

要注意的是代碼庫的代碼,包含了(音頻、緩存、幀率控制等),而且相對文章也有一些小改動(目的是方便使用,但是稍微增加了點代碼量),動手操作起來吧。


  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式師世界 All Rights Reserved