在介绍如何用 Python3 & OpenCV 将视频转成字符动画之前,先简单的介绍一下 OpenCV 吧,毕竟可能很多小伙伴不太了解:
百度百科:
OpenCV是一个基于BSD许可(开源)发行的跨平台计算机视觉库,可以运行在Linux、Windows和Mac OS操作系统上。它轻量级而且高效——由一系列 C 函数和少量 C++ 类构成,同时提供了Python、Ruby、MATLAB等语言的接口,实现了图像处理和计算机视觉方面的很多通用算法。
接着来看看最终的效果图吧,很可爱又很酷炫;
效果展示
(由于是在线环境,流畅度是不及本地环境的)
播放停止后的效果,注意终端中并无残留的动画字符:
说明:
知识点
通过开发这个项目可以学习到如下的知识点:
- OpenCV 编译;
- 使用 OpenCV 处理图片、视频;
- 图片转字符画原理;
- 守护线程;
- 光标定位转义编码;
编译安装 OpenCV
本课程的实验中使用了 OpenCV 3.1,因此我们需要编译安装它。
首先我们需要处理一个问题:当前实验楼的环境中 python3 命令使用的 python 版本为 3.5,但源中没有 python3.5-dev 的包,这会导致编译 numpy、OpenCV 出错,以及可能会出现其它问题。总而言之,我们需要将 python3 命令使用的 python 版本切换为 3.4。
$ sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.4 70 --slave /usr/bin/python3m python3m /usr/bin/python3.4m
$ sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.4 70 --slave /usr/bin/python3m python3m /usr/bin/python3.4m
然后安装一些依赖的包:
$ sudo apt-get update
$ sudo apt-get install python3-dev
$ sudo pip3 install numpy
$ sudo apt-get install cmake libgtk2.0-dev pkg-config libavcodec-dev libavformat-dev libswscale-dev
$ sudo apt-get update
$ sudo apt-get install python3-dev
$ sudo pip3 install numpy
$ sudo apt-get install cmake libgtk2.0-dev pkg-config libavcodec-dev libavformat-dev libswscale-dev
现在可以开始编译 OpenCV 3.1 了,不过这个编译过程极其耗费时间(2个小时多),我相信各位同学们是没有耐心等待编译完成的,所以我在后面提供了编译后的二进制文件下载链接,大家直接使用就可以了。
下面是在实验楼环境中编译 OpenCV 3.1 所需的命令,其他环境中的编译请参考官网:http://docs.opencv.org/master/d7/d9f/tutorial_linux_install.html
$ wget https://github.com/Itseez/opencv/archive/3.1.0.zip
$ unzip 3.1.0.zip && cd opencv-3.1.0/
$ mkdir build && cd build
$ cmake -D CMAKE_BUILD_TYPE=Release \
-D CMAKE_INSTALL_PREFIX=/usr/local \
PYTHON3_EXECUTABLE=/usr/bin/python3 \
PYTHON_INCLUDE_DIR=/usr/include/python3.4 \
PYTHON_LIBRARY=/usr/lib/x86_64-linux-gnu/libpython3.4m.so \
PYTHON3_NUMPY_INCLUDE_DIRS=/usr/local/lib/python3.4/dist-packages/numpy/core/include ..
$ make -j4
$ wget https://github.com/Itseez/opencv/archive/3.1.0.zip
$ unzip 3.1.0.zip && cd opencv-3.1.0/
$ mkdir build && cd build
$ cmake -D CMAKE_BUILD_TYPE=Release \
-D CMAKE_INSTALL_PREFIX=/usr/local \
PYTHON3_EXECUTABLE=/usr/bin/python3 \
PYTHON_INCLUDE_DIR=/usr/include/python3.4 \
PYTHON_LIBRARY=/usr/lib/x86_64-linux-gnu/libpython3.4m.so \
PYTHON3_NUMPY_INCLUDE_DIRS=/usr/local/lib/python3.4/dist-packages/numpy/core/include ..
$ make -j4
不想自己编译的同学请下载编译好的二进制文件,然后解压并进入opencv-3.1.0/build
目录:
$ wget http://labfile.oss.aliyuncs.com/courses/637/opencv-3.1.0.tar.gz
$ tar xzvf opencv-3.1.0.tar.gz
$ cd opencv-3.1.0/build
$ wget http://labfile.oss.aliyuncs.com/courses/637/opencv-3.1.0.tar.gz
$ tar xzvf opencv-3.1.0.tar.gz
$ cd opencv-3.1.0/build
然后我们开始安装:
$ sudo make install
$ sudo make install
程序原理
大家应该都明白视频其实可以看作一系列图片组成的,因此视频转字符动画最基本的便是图片转字符画,这一部分内容也在Python 图片转字符画 课程中有讲过。
在这里简单的说一下图片转字符画的原理:首先将图片转为灰度图,每个像素都只有亮度信息(用 0~255 表示)。然后我们构建一个有限字符集合,其中的每一个字符都与一段亮度范围对应,我们便可以根据此对应关系以及像素的亮度信息把每一个像素用对应的字符表示,这样字符画就形成了。
字符动画要能播放才有意义。最最简单粗暴的,用文本编辑器打开字符动画文本文件,然后狂按 PageDown 键就能播放。然而这真的太简单太粗暴了,一点都不优雅。
我们还是在终端里面播放字符动画,只需要一帧一帧输出就能达到动画的效果了,然而这却有一个很大的弊端:播放时,你会发现终端右边的滚动条会越来越小(如果有的话);播放完毕后,在终端中往上翻页,全是之前输出的字符画,播放前的命令历史全部被挤占掉了。在本实验后面提供了这个问题的解决办法。
完整代码
import sys
import os
import time
import threading
import cv2
import pyprind
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=-1, fill=False, wrap=False):
if limitSize != -1 and (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
class I2Char(CharFrame):
result = None
def __init__(self, path, limitSize=-1, fill=False, wrap=False):
self.genCharImage(path, limitSize, fill, wrap)
def genCharImage(self, path, limitSize=-1, fill=False, wrap=False):
img = cv2.imread(path, cv2.IMREAD_GRAYSCALE)
if img is None:
return
self.result = self.convert(img, limitSize, fill, wrap)
def show(self, stream = 2):
if self.result is None:
return
if stream == 1 and os.isatty(sys.stdout.fileno()):
self.streamOut = sys.stdout.write
self.streamFlush = sys.stdout.flush
elif stream == 2 and os.isatty(sys.stderr.fileno()):
self.streamOut = sys.stderr.write
self.streamFlush = sys.stderr.flush
elif hasattr(stream, 'write'):
self.streamOut = stream.write
self.streamFlush = stream.flush
self.streamOut(self.result)
self.streamFlush()
self.streamOut('\n')
class V2Char(CharFrame):
charVideo = []
timeInterval = 0.033
def __init__(self, path):
if path.endswith('txt'):
self.load(path)
else:
self.genCharVideo(path)
def genCharVideo(self, filepath):
self.charVideo = []
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)):
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()
def export(self, filepath):
if not self.charVideo:
return
with open(filepath,'w') as f:
for frame in self.charVideo:
# 加一个换行符用以分隔每一帧
f.write(frame + '\n')
def load(self, filepath):
self.charVideo = []
# 一行即为一帧
for i in open(filepath):
self.charVideo.append(i[:-1])
def play(self, stream = 1):
# Bug:
# 光标定位转义编码不兼容 Windows
if not self.charVideo:
return
if stream == 1 and os.isatty(sys.stdout.fileno()):
self.streamOut = sys.stdout.write
self.streamFlush = sys.stdout.flush
elif stream == 2 and os.isatty(sys.stderr.fileno()):
self.streamOut = sys.stderr.write
self.streamFlush = sys.stderr.flush
elif hasattr(stream, 'write'):
self.streamOut = stream.write
self.streamFlush = stream.flush
breakflag = False
def getChar():
nonlocal breakflag
try:
# 若系统为 windows 则直接调用 msvcrt.getch()
import msvcrt
except ImportError:
import termios, tty
# 获得标准输入的文件描述符
fd = sys.stdin.fileno()
# 保存标准输入的属性
old_settings = termios.tcgetattr(fd)
try:
# 设置标准输入为原始模式
tty.setraw(sys.stdin.fileno())
# 读取一个字符
ch = sys.stdin.read(1)
finally:
# 恢复标准输入为原来的属性
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
if ch:
breakflag = True
else:
if msvcrt.getch():
breakflag = True
# 创建线程
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')
if __name__ == '__main__':
import argparse
# 设置命令行参数
parser = argparse.ArgumentParser()
parser.add_argument('file',
help='Video file or charvideo file')
parser.add_argument('-e', '--export', nargs = '?', const = 'charvideo.txt',
help='Export charvideo file')
# 获取参数
args = parser.parse_args()
v2char = V2Char(args.file)
if args.export:
v2char.export(args.export)
v2char.play()
import sys
import os
import time
import threading
import cv2
import pyprind
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=-1, fill=False, wrap=False):
if limitSize != -1 and (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
class I2Char(CharFrame):
result = None
def __init__(self, path, limitSize=-1, fill=False, wrap=False):
self.genCharImage(path, limitSize, fill, wrap)
def genCharImage(self, path, limitSize=-1, fill=False, wrap=False):
img = cv2.imread(path, cv2.IMREAD_GRAYSCALE)
if img is None:
return
self.result = self.convert(img, limitSize, fill, wrap)
def show(self, stream = 2):
if self.result is None:
return
if stream == 1 and os.isatty(sys.stdout.fileno()):
self.streamOut = sys.stdout.write
self.streamFlush = sys.stdout.flush
elif stream == 2 and os.isatty(sys.stderr.fileno()):
self.streamOut = sys.stderr.write
self.streamFlush = sys.stderr.flush
elif hasattr(stream, 'write'):
self.streamOut = stream.write
self.streamFlush = stream.flush
self.streamOut(self.result)
self.streamFlush()
self.streamOut('\n')
class V2Char(CharFrame):
charVideo = []
timeInterval = 0.033
def __init__(self, path):
if path.endswith('txt'):
self.load(path)
else:
self.genCharVideo(path)
def genCharVideo(self, filepath):
self.charVideo = []
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)):
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()
def export(self, filepath):
if not self.charVideo:
return
with open(filepath,'w') as f:
for frame in self.charVideo:
# 加一个换行符用以分隔每一帧
f.write(frame + '\n')
def load(self, filepath):
self.charVideo = []
# 一行即为一帧
for i in open(filepath):
self.charVideo.append(i[:-1])
def play(self, stream = 1):
# Bug:
# 光标定位转义编码不兼容 Windows
if not self.charVideo:
return
if stream == 1 and os.isatty(sys.stdout.fileno()):
self.streamOut = sys.stdout.write
self.streamFlush = sys.stdout.flush
elif stream == 2 and os.isatty(sys.stderr.fileno()):
self.streamOut = sys.stderr.write
self.streamFlush = sys.stderr.flush
elif hasattr(stream, 'write'):
self.streamOut = stream.write
self.streamFlush = stream.flush
breakflag = False
def getChar():
nonlocal breakflag
try:
# 若系统为 windows 则直接调用 msvcrt.getch()
import msvcrt
except ImportError:
import termios, tty
# 获得标准输入的文件描述符
fd = sys.stdin.fileno()
# 保存标准输入的属性
old_settings = termios.tcgetattr(fd)
try:
# 设置标准输入为原始模式
tty.setraw(sys.stdin.fileno())
# 读取一个字符
ch = sys.stdin.read(1)
finally:
# 恢复标准输入为原来的属性
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
if ch:
breakflag = True
else:
if msvcrt.getch():
breakflag = True
# 创建线程
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')
if __name__ == '__main__':
import argparse
# 设置命令行参数
parser = argparse.ArgumentParser()
parser.add_argument('file',
help='Video file or charvideo file')
parser.add_argument('-e', '--export', nargs = '?', const = 'charvideo.txt',
help='Export charvideo file')
# 获取参数
args = parser.parse_args()
v2char = V2Char(args.file)
if args.export:
v2char.export(args.export)
v2char.play()
最后
作者:实验楼