既然要开始做东西,首要的问题就是想好要怎么做,大家都知道视频是由一系列图片一帧一帧组成的,因此视频转字符动画最基本的便是图片转字符画。
在这里简单的说一下图片转字符画的原理:首先将图片转为灰度图,每个像素都只有亮度信息(用 0~255 表示)。然后我们构建一个有限字符集合,其中的每一个字符都与一段亮度范围对应,我们便可以根据此对应关系以及像素的亮度信息把每一个像素用对应的字符表示,这样字符画就形成了。
Tips:如果对"灰度图像"这个概念不太理解的可以查阅百度百科
计算一张图片的灰度图像的方法如下(来自百度百科):
Python学习交流群:1004391443
所以我们要做的就只是让字符画在命令行里面动起来就可以了。
Tips:图片转字符画可以参考:https://www.shiyanlou.com/courses/37
环境和工具:vscode、Mac OS、Python 3.7
这次实验使用到的核心的库是OpenCV-Python
Tips:这里分享一个我觉得还不错的OpenCV-Python的中文文档:https://www.kancloud.cn/aollo/aolloopencv/269602
实验
实验开始前我们需要安装 OpenCV-Python 的包:
pip install opencv-python
读取视频:
def genCharVideo(self, filepath): self.charVideo =[] # 用opencv读取视频 cap = cv2.VideoCapture(filepath) self.timeInterval = round(1/ cap.get(5),3) nf = int(cap.get(7)) print('Generate char video, please wait...') for i in pyprind.prog_bar(range(nf)): # 转换颜色空间,第二个参数是转换类型,cv2.COLOR_BGR2GRAY表示从BGR↔Gray rawFrame = cv2.cvtColor(cap.read()[1], cv2.COLOR_BGR2GRAY) frame = self.convert(rawFrame, os.get_terminal_size(), fill=True) self.charVideo.append(frame) cap.release()
这里的VideoCapture是用来读取视频的, cv2.cvtColor(input_imageflag)用于转换颜色空间,其中flag就是转换类型。对于 BGR↔Gray 的转换,我们使用的 flag 就是 cv2.COLORBGR2GRAY。对于 BGR↔HSV 的转换我们用的 flag 就是 cv2.COLORBGR2HSV。
将帧转换成字符画
ascii_frame 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=-1, fill=False, wrap=False): if limitSize !=-1and(img.shape[0]> limitSize[1]or img.shape[1]> limitSize[0]): img = cv2.resize(img, limitSize, interpolation=cv2.INTER_AREA) 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
这段代码其实就是将已经转变的灰度图的像素值映射到 ascii_char上,然后输出到控制台。
控制输出
# 创建线程 getchar = threading.Thread(target=getChar) # 设置为守护线程 getchar.daemon =True # 启动守护线程 getchar.start() # 输出的字符画行数 rows = len(self.charVideo[0])// os.get_terminal_size()[0] for frame in self.charVideo: # 接收到输入则退出循环 if breakflag: break self.streamOut(frame) self.streamFlush() time.sleep(self.timeInterval) # 共 rows 行,光标上移 rows-1 行回到开始处 self.streamOut('\033[{}A\r'.format(rows -1)) # 光标下移 rows-1 行到最后一行,清空最后一行 self.streamOut('\033[{}B\033[K'.format(rows -1)) # 清空最后一帧的所有行(从倒数第二行起) for i in range(rows -1): # 光标上移一行 self.streamOut('\033[1A') # 清空光标所在行 self.streamOut('\r\033[K') if breakflag: self.streamOut('User interrupt!\n') else: self.streamOut('Finished!\n')
执行
最后在main函数中设置下要读取的文件名,再Play一下就可以了。