目錄
文本信息
視頻有一幀一幀的畫面構成,利用字符替代畫面中的像素信息,就可以將視頻轉換爲字符畫的視頻,本文將利用python庫,對一段視頻進行轉換,將其轉換爲字符串展示的視頻,本文在文章——Python將視頻轉換爲全字符視頻(含音頻)的參考下完成。
項目所用主要庫
opencv ——參考文檔
pillow——參考文檔
re
視頻轉換的原理
將視頻轉換爲字符畫,原理是利用像素點信息將其映射到字符上,對於彩色視頻,需要對其進行處理,將其轉換爲灰度視頻,其轉換原理爲利用灰度值公式將彩色的RGB像素表示爲灰度值。最後再利用ascii碼,將灰度值映射爲字符。
gray = 0.2126 * r + 0.7152 * g + 0.0722 * b
ascii_char = "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,\"^`'. "
完整代碼
import cv2
import time
import os
from PIL import Image,ImageFont,ImageDraw
from cv2 import VideoWriter,VideoWriter_fourcc,imread,resize
import re
#轉換爲字符畫
class CharFrame:
ascii_char = "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,\"^`'. "
# 像素映射到字符
def pixelToChar(self, luminance):
return self.ascii_char[int(luminance / 256 * len(self.ascii_char))]
# 將普通幀轉爲 ASCII 字符幀
def convert(self, img, limitSize=(),fill=False,wrap=False):
if limitSize != () and (img.shape[0] > limitSize[1] or img.shape[1] > limitSize[0]):
#對圖像縮放到適合終端的大小
img = cv2.resize(img, limitSize, interpolation=cv2.INTER_NEAREST)
ascii_frame = ''
blank = ''
if fill:
blank += ' ' * (limitSize[0] - img.shape[1])
if wrap:
blank += '\n'
for i in range(img.shape[0]):
for j in range(img.shape[1]):
ascii_frame += self.pixelToChar(img[i, j])
ascii_frame += blank
return ascii_frame
class V2Char(CharFrame):
charVideo = []
def __init__(self, path,size=(120,30)):
"""
:param path:視頻文件路徑
:param size: 所要縮放的大小,(width,height)
"""
self.path=path
self.size=size
self.genCharVideo(path)
def genCharVideo(self, filepath):
self.charVideo = []
# 用opencv讀取視頻,設置相機
cap = cv2.VideoCapture(filepath)
#get(5)爲視頻的幀速率
self.FPS=cap.get(cv2.CAP_PROP_FPS)#或者用get(5)
#get(7)爲視頻的幀數
nf = int(cap.get(7))
print('Generate char video, please wait...')
for i in range(nf):
# 轉換顏色空間,第二個參數是轉換類型,cv2.COLOR_BGR2GRAY表示從BGR↔Gray
#read()讀取視頻畫面,第一個參數表示讀取是否成功,第二個即爲畫面,所以索引用1
#每read一次讀取一個畫面
rawFrame = cv2.cvtColor(cap.read()[1], cv2.COLOR_BGR2GRAY)
frame = self.convert(rawFrame,self.size,fill=True)#注意這裏的參數輸入
self.charVideo.append(frame)
#釋放相機
cap.release()
print('Finished.')
#設置最大的單字符長度
def max_width(self):
font = ImageFont.truetype(os.path.join("fonts", "arial.ttf"), 18)
font_size=[]
for i in self.ascii_char:
font_size.append(font.getsize(i)[0])
return max(font_size)
#對一行切分爲數組的形式,即表示爲一個畫面
def str_slice(self,char,row,col):
slice_char=[]
for i in range(row):
slice_char.append(char[i*col:(i+1)*col])
return slice_char
def str2fig(self):
#很多字符的寬度不一樣,需要設置字符最大的寬度
font = ImageFont.truetype('arial.ttf', 18)
le_width=self.max_width()
line_height=font.getsize(self.charVideo[0])[1]
#上面每3600個字符爲一個畫面,在列表中爲一行,30*120
col,row=self.size
#畫布參數設置
img_height=line_height*(row+1)
img_width=le_width*(col+1)
#處理過的圖片保存位置,以視頻的名字命名文件夾
catalog=self.path.split('.')[0]
self.mkdir(catalog)
#繪製圖片
for p_id,char in enumerate(self.charVideo):
div_char=self.str_slice(char,row,col)
im = Image.new("RGB", (img_width, img_height), (255, 255, 255))
dr = ImageDraw.Draw(im)
for i,str in enumerate(div_char):
for j in range(len(str)):
dr.text((5+j*le_width,5+i*line_height), str[j],
font=font, fill="#000000")
im.save(catalog+r'\pic_{}.jpg'.format(p_id))
def mkdir(self,catalog):
isExists = os.path.exists(catalog)
if not isExists:
os.makedirs(catalog)
return True
else:
return False
def jpg2video(self):
catalog=self.path.split('.')[0]
#利用os.listdir()讀取文件有亂序的問題,需要對其進行排序
images=os.listdir(catalog)
images.sort(key=lambda x: int(re.findall(r'\d+', x)[0]))
im=Image.open(catalog+'\\'+images[0])
fourcc = VideoWriter_fourcc(*"MJPG")
vw = VideoWriter('stringvideo' + '.avi', fourcc,self.FPS, im.size)
for image in images:
frame=cv2.imread(catalog+'\\'+image)
vw.write(frame)
vw.release()
#遞歸刪除目錄
def remove_dir(self,path):
if os.path.exists(path):
if os.path.isdir(path):
dirs = os.listdir(path)
for d in dirs:
if os.path.isdir(path+'/'+d):
self.remove_dir(path+'/'+d)
elif os.path.isfile(path+'/'+d):
os.remove(path+'/'+d)
os.rmdir(path)
return
elif os.path.isfile(path):
os.remove(path)
return
def gen_video(self):
self.str2fig()
print('str2fig Finished')
print('start generate video.wait...')
self.jpg2video()
print('generate video Finished')
print('start delete catalog.wait...')
self.remove_dir(self.path.split('.')[0])
print('delete cache catalog finished')
if __name__ == '__main__':
#修改你索要輸入的文件的路徑
v2char = V2Char(r'D:\pycharm_work\img_relad\video.mp4')
v2char.gen_video()
代碼中值得注意的事項
1)、cv2.resize():第一個參數表示畫面信息,第二個參數limitSize表示要對畫面進行的縮放大小,其格式應該爲(width,height),即像素的列和行
2)、視頻幀率的獲取:利用以下代碼得到,cap.get()的參見參數參考文章——OpenCV VideoCapture.get()參數詳解
cap = cv2.VideoCapture(filepath)
self.FPS=cap.get(cv2.CAP_PROP_FPS)
3)、在將視頻處理爲字符串的表示之後,要再將字符串寫爲圖片保存,由於所用的字符有不同的長度,因此直接整段寫入的話會有圖片混亂的問題,因此用max_width()函數統一字符在圖片上的表示長度,在寫入圖片時,按照像素點進行處理。即代碼的str2fig()的三個for循環片段。因此在生成畫面時比較慢。
4)、os.listdir()函數用來讀取保存的圖片,由於其返回的可能是亂序的,因此要對其中的內容進行排序。比如,本次處理的視頻共有1400餘幀,用os.listdir()讀取後返回的文件名列表大概如下:
['pic_0.jpg', 'pic_1.jpg', 'pic_10.jpg', 'pic_100.jpg', 'pic_1000.jpg', 'pic_1001.jpg', 'pic_1002.jpg', 'pic_1003.jpg', 'pic_1004.jpg', 'pic_1005.jpg', 'pic_1006.jpg', 'pic_1007.jpg', 'pic_1008.jpg', ....'pic_999.jpg']
直接讀取圖片生成視頻會有明顯的視頻時間線不同步問題,利用re模塊處理所讀到的文件名,提取其中的數字,利用sort函數進行排序,得到有序的文件名列表,其中re.findall()函數返回值爲一個列表,需要提取其中的內容,並用int()轉爲數字。
5)、cv2.imread()函數要求輸入文件的完整路徑,否則無法讀取圖片,遇到視頻生成失敗時,檢查frame是否爲None,是的話可能是文件名輸入出錯等情況導致
不足之處
在函數str2fig()中,將畫布和畫筆的打開都放在了for循環中,這導致每次打開處理一個畫面就要打開一次畫筆,因此運行速度比較慢,本來的計劃是將畫筆畫布放在循環之外,這樣就可以不用每次都打開了。但在實際運行中發現,這樣的運行方式導致,後來生成的畫面中有前一幀的信息沒有擦除,需要在保存畫面後,將畫布清空,但在網上沒有找到清除畫布內容的方法,因此將其放到了循環內。
im = Image.new("RGB", (img_width, img_height), (255, 255, 255))
dr = ImageDraw.Draw(im)
for p_id, char in enumerate(self.charVideo):
div_char = self.str_slice(char, row, col)
for i, str in enumerate(div_char):
for j in range(len(str)):
dr.text((5 + j * le_width, 5 + i * line_height), str[j],
font=font, fill="#000000")
im.save(catalog + r'\pic_{}.jpg'.format(p_id))